diff options
Diffstat (limited to 'src/boot')
92 files changed, 18748 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..38b2c3a --- /dev/null +++ b/src/boot/bless-boot-generator.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <unistd.h> + +#include "efi-loader.h" +#include "generator.h" +#include "initrd-util.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()) { + log_debug("Skipping generator, running in the initrd."); + return EXIT_SUCCESS; + } + + 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..0c0b4f2 --- /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 "build.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 "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 = -EBADF; + + 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 = -EBADF; + + 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..4ff91cb --- /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 "build.h" +#include "bus-error.h" +#include "log.h" +#include "main-func.h" +#include "pretty-print.h" +#include "terminal-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-install.c b/src/boot/bootctl-install.c new file mode 100644 index 0000000..bacbbb2 --- /dev/null +++ b/src/boot/bootctl-install.c @@ -0,0 +1,1101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bootctl.h" +#include "bootctl-install.h" +#include "bootctl-random-seed.h" +#include "bootctl-util.h" +#include "chase.h" +#include "copy.h" +#include "dirent-util.h" +#include "efi-api.h" +#include "env-file.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "glyph-util.h" +#include "id128-util.h" +#include "os-util.h" +#include "path-util.h" +#include "rm-rf.h" +#include "stat-util.h" +#include "sync-util.h" +#include "tmpfile-util.h" +#include "umask-util.h" +#include "utf8.h" + +static int load_etc_machine_id(void) { + int r; + + r = sd_id128_get_machine(&arg_machine_id); + if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) /* 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 *p = NULL, *s = NULL, *layout = NULL; + int r; + + p = path_join(arg_root, "etc/machine-info"); + if (!p) + return log_oom(); + + r = parse_env_file(NULL, p, + "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 /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, *p = NULL; + int r; + + p = path_join(arg_root, etc_kernel(), "install.conf"); + if (!p) + return log_oom(); + + r = parse_env_file(NULL, p, "layout", &layout); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to parse %s: %m", p); + + if (!isempty(layout)) { + log_debug("layout=%s is specified in %s.", layout, p); + free_and_replace(arg_install_layout, layout); + } + + 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 == BOOT_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; +} + +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 == -ESRCH) + return log_notice_errno(r, "Source file \"%s\" does not carry version information!", from); + if (r < 0) + return r; + + r = get_file_version(fd_to, &b); + if (r == -ESRCH) + return log_notice_errno(r, "Skipping \"%s\", it's owned by another boot loader (no version info found).", + to); + if (r < 0) + return r; + if (compare_product(a, b) != 0) + return log_notice_errno(SYNTHETIC_ERRNO(ESRCH), + "Skipping \"%s\", 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\", newer boot loader version in place already.", to); + if (r == 0) + return log_info_errno(SYNTHETIC_ERRNO(ESTALE), + "Skipping \"%s\", 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 = -EBADF, fd_to = -EBADF; + _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) < 0) + 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(); + + 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(t); + return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", from, t); + } + + r = RET_NERRNO(renameat(AT_FDCWD, t, AT_FDCWD, to)); + if (r < 0) { + (void) unlink(t); + return log_error_errno(r, "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(p, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &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(p, NULL, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &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(q, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|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(v, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|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_and_opendir(BOOTLIBDIR, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &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_and_opendir(BOOTLIBDIR, NULL, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &path, &d); + if (r == -ENOENT && arg_graceful) { + log_debug("Source directory does not exist, ignoring."); + return 0; + } + 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, -ESRCH)) + continue; + RET_GATHER(r, k); + } + + return r; +} + +static int install_loader_config(const char *esp_path) { + _cleanup_(unlink_and_freep) char *t = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(arg_make_entry_directory >= 0); + + p = path_join(esp_path, "/loader/loader.conf"); + 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, "#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, LINK_TMPFILE_SYNC); + 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, LINK_TMPFILE_SYNC); + 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) { + _cleanup_free_ char* p = NULL; + 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 == BOOT_ENTRY_TOKEN_MACHINE_ID) + return 0; + + p = path_join(arg_root, etc_kernel(), "entry-token"); + if (!p) + return log_oom(); + + r = write_string_file(p, 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 %s: %m", arg_entry_token, p); + + return 0; +} + +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 const char *pick_efi_boot_option_description(void) { + return arg_efi_boot_option_description ?: "Linux Boot Manager"; +} + +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_and_access(path, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, 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 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; +} + +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(); + + 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 remove_boot_efi(const char *esp_path) { + _cleanup_closedir_ DIR *d = NULL; + _cleanup_free_ char *p = NULL; + int r, c = 0; + + r = chase_and_opendir("/EFI/BOOT", esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &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 = -EBADF; + _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 == -ESRCH) + continue; /* No version information */ + if (r < 0) + return r; + if (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(LoaderConfigConsoleMode), + EFI_LOADER_VARIABLE(LoaderConfigTimeout), + EFI_LOADER_VARIABLE(LoaderConfigTimeoutOneShot), + EFI_LOADER_VARIABLE(LoaderEntryDefault), + EFI_LOADER_VARIABLE(LoaderEntryLastBooted), + 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; +} + +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; +} + +int verb_is_installed(int argc, char *argv[], void *userdata) { + int r; + + r = acquire_esp(/* unprivileged_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; + } +} diff --git a/src/boot/bootctl-install.h b/src/boot/bootctl-install.h new file mode 100644 index 0000000..cd4b725 --- /dev/null +++ b/src/boot/bootctl-install.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_install(int argc, char *argv[], void *userdata); +int verb_remove(int argc, char *argv[], void *userdata); +int verb_is_installed(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-random-seed.c b/src/boot/bootctl-random-seed.c new file mode 100644 index 0000000..cfe10c4 --- /dev/null +++ b/src/boot/bootctl-random-seed.c @@ -0,0 +1,239 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bootctl.h" +#include "bootctl-random-seed.h" +#include "bootctl-util.h" +#include "efi-api.h" +#include "env-util.h" +#include "fd-util.h" +#include "find-esp.h" +#include "fs-util.h" +#include "glyph-util.h" +#include "io-util.h" +#include "mkdir.h" +#include "path-util.h" +#include "random-util.h" +#include "sha256.h" +#include "tmpfile-util.h" +#include "umask-util.h" + +static int random_seed_verify_permissions(int fd, mode_t expected_type) { + _cleanup_free_ char *full_path = NULL; + struct stat st; + int r; + + assert(fd >= 0); + + r = fd_get_path(fd, &full_path); + if (r < 0) + return log_error_errno(r, "Unable to determine full path of random seed fd: %m"); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Unable to stat %s: %m", full_path); + + if (((st.st_mode ^ expected_type) & S_IFMT) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADF), + "Unexpected inode type when validating random seed access mode on %s: %m", full_path); + + if ((st.st_mode & 0007) == 0) /* All world bits are off? Then all is good */ + return 0; + + if (S_ISREG(expected_type)) + log_warning("%s Random seed file '%s' is world accessible, which is a security hole! %s", + special_glyph(SPECIAL_GLYPH_WARNING_SIGN), full_path, special_glyph(SPECIAL_GLYPH_WARNING_SIGN)); + else { + assert(S_ISDIR(expected_type)); + log_warning("%s Mount point '%s' which backs the random seed file is world accessible, which is a security hole! %s", + special_glyph(SPECIAL_GLYPH_WARNING_SIGN), full_path, special_glyph(SPECIAL_GLYPH_WARNING_SIGN)); + } + + return 1; +} + +static int set_system_token(void) { + uint8_t buffer[RANDOM_EFI_SEED_SIZE]; + size_t token_size; + int r; + + if (!arg_touch_variables) + return 0; + + if (arg_root) { + log_warning("Acting on %s, skipping EFI variable setup.", + arg_image ? "image" : "root directory"); + return 0; + } + + if (!is_efi_boot()) { + log_notice("Not booted with EFI, skipping EFI variable setup."); + 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."); + } 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 >= sizeof(buffer)) { + /* 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, sizeof(buffer)); + } + + r = crypto_random_bytes(buffer, sizeof(buffer)); + 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. */ + WITH_UMASK(0077) { + r = efi_set_variable(EFI_LOADER_VARIABLE(LoaderSystemToken), buffer, sizeof(buffer)); + if (r < 0) { + if (!arg_graceful) + return log_error_errno(r, "Failed to write 'LoaderSystemToken' EFI variable: %m"); + + if (r == -EINVAL) + log_notice_errno(r, "Unable to write 'LoaderSystemToken' EFI variable (firmware problem?), ignoring: %m"); + else + log_notice_errno(r, "Unable to write 'LoaderSystemToken' EFI variable, ignoring: %m"); + } else + log_info("Successfully initialized system token in EFI variable with %zu bytes.", sizeof(buffer)); + } + + return 0; +} + +int install_random_seed(const char *esp) { + _cleanup_close_ int esp_fd = -EBADF, loader_dir_fd = -EBADF, fd = -EBADF; + _cleanup_free_ char *tmp = NULL; + uint8_t buffer[RANDOM_EFI_SEED_SIZE]; + struct sha256_ctx hash_state; + bool refreshed, warned = false; + int r; + + assert(esp); + + assert_cc(RANDOM_EFI_SEED_SIZE == SHA256_DIGEST_SIZE); + + esp_fd = open(esp, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (esp_fd < 0) + return log_error_errno(errno, "Failed to open ESP directory '%s': %m", esp); + + (void) random_seed_verify_permissions(esp_fd, S_IFDIR); + + loader_dir_fd = open_mkdir_at(esp_fd, "loader", O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_NOFOLLOW, 0775); + if (loader_dir_fd < 0) + return log_error_errno(loader_dir_fd, "Failed to open loader directory '%s/loader': %m", esp); + + r = crypto_random_bytes(buffer, sizeof(buffer)); + if (r < 0) + return log_error_errno(r, "Failed to acquire random seed: %m"); + + sha256_init_ctx(&hash_state); + sha256_process_bytes_and_size(buffer, sizeof(buffer), &hash_state); + + fd = openat(loader_dir_fd, "random-seed", O_NOFOLLOW|O_CLOEXEC|O_RDONLY|O_NOCTTY); + if (fd < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open old random seed file: %m"); + + sha256_process_bytes(&(const ssize_t) { 0 }, sizeof(ssize_t), &hash_state); + refreshed = false; + } else { + ssize_t n; + + warned = random_seed_verify_permissions(fd, S_IFREG) > 0; + + /* Hash the old seed in so that we never regress in entropy. */ + + n = read(fd, buffer, sizeof(buffer)); + if (n < 0) + return log_error_errno(errno, "Failed to read old random seed file: %m"); + + sha256_process_bytes_and_size(buffer, n, &hash_state); + + fd = safe_close(fd); + refreshed = n > 0; + } + + sha256_finish_ctx(&hash_state, buffer); + + if (tempfn_random("random-seed", "bootctl", &tmp) < 0) + return log_oom(); + + fd = openat(loader_dir_fd, tmp, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|O_WRONLY|O_CLOEXEC, 0600); + if (fd < 0) + return log_error_errno(fd, "Failed to open random seed file for writing: %m"); + + if (!warned) /* only warn once per seed file */ + (void) random_seed_verify_permissions(fd, S_IFREG); + + r = loop_write(fd, buffer, sizeof(buffer)); + if (r < 0) { + log_error_errno(r, "Failed to write random seed file: %m"); + goto fail; + } + + if (fsync(fd) < 0 || fsync(loader_dir_fd) < 0) { + r = log_error_errno(errno, "Failed to sync random seed file: %m"); + goto fail; + } + + if (renameat(loader_dir_fd, tmp, loader_dir_fd, "random-seed") < 0) { + r = log_error_errno(errno, "Failed to move random seed file into place: %m"); + goto fail; + } + + tmp = mfree(tmp); + + if (syncfs(fd) < 0) + return log_error_errno(errno, "Failed to sync ESP file system: %m"); + + log_info("Random seed file %s/loader/random-seed successfully %s (%zu bytes).", esp, refreshed ? "refreshed" : "written", sizeof(buffer)); + + return set_system_token(); + +fail: + assert(tmp); + (void) unlinkat(loader_dir_fd, tmp, 0); + + return r; +} + +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; + + return 0; +} diff --git a/src/boot/bootctl-random-seed.h b/src/boot/bootctl-random-seed.h new file mode 100644 index 0000000..91596d3 --- /dev/null +++ b/src/boot/bootctl-random-seed.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int install_random_seed(const char *esp); + +int verb_random_seed(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-reboot-to-firmware.c b/src/boot/bootctl-reboot-to-firmware.c new file mode 100644 index 0000000..91f2597 --- /dev/null +++ b/src/boot/bootctl-reboot-to-firmware.c @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bootctl-reboot-to-firmware.h" +#include "efi-api.h" +#include "parse-util.h" + +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 0; /* 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; + } +} diff --git a/src/boot/bootctl-reboot-to-firmware.h b/src/boot/bootctl-reboot-to-firmware.h new file mode 100644 index 0000000..0ca4b2c --- /dev/null +++ b/src/boot/bootctl-reboot-to-firmware.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +int verb_reboot_to_firmware(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-set-efivar.c b/src/boot/bootctl-set-efivar.c new file mode 100644 index 0000000..cb2ed0d --- /dev/null +++ b/src/boot/bootctl-set-efivar.c @@ -0,0 +1,171 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <uchar.h> +#include <unistd.h> + +#include "bootctl.h" +#include "bootctl-set-efivar.h" +#include "efivars.h" +#include "efi-loader.h" +#include "stdio-util.h" +#include "utf8.h" +#include "virt.h" + +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; + bool menu_disabled = false; + int r; + + assert(arg1); + assert(ret_timeout); + assert(ret_timeout_size); + + assert_cc(STRLEN("menu-disabled") < ELEMENTSOF(utf8)); + + /* Note: Since there is no way to query if the bootloader supports the string tokens, we explicitly + * set their numerical value(s) instead. This means that some of the sd-boot internal ABI has leaked + * although the ship has sailed and the side-effects are self-contained. + */ + if (streq(arg1, "menu-force")) + timeout = USEC_INFINITY; + else if (streq(arg1, "menu-hidden")) + timeout = 0; + else if (streq(arg1, "menu-disabled")) { + uint64_t loader_features = 0; + + (void) efi_loader_get_features(&loader_features); + if (!(loader_features & EFI_LOADER_FEATURE_MENU_DISABLE)) { + if (!arg_graceful) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Loader does not support 'menu-disabled': %m"); + + log_warning("Loader does not support 'menu-disabled', setting anyway."); + } + menu_disabled = true; + } 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."); + } + + if (menu_disabled) + xsprintf(utf8, "menu-disabled"); + else + xsprintf(utf8, USEC_FMT, MIN(timeout / USEC_PER_SEC, UINT32_MAX)); + + encoded = utf8_to_utf16(utf8, SIZE_MAX); + 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, SIZE_MAX); + if (!encoded) + return log_oom(); + + *ret_target = encoded; + *ret_target_size = char16_strlen(encoded) * 2 + 2; + } + + return 0; +} + +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; +} diff --git a/src/boot/bootctl-set-efivar.h b/src/boot/bootctl-set-efivar.h new file mode 100644 index 0000000..6441681 --- /dev/null +++ b/src/boot/bootctl-set-efivar.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_set_efivar(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-status.c b/src/boot/bootctl-status.c new file mode 100644 index 0000000..d171512 --- /dev/null +++ b/src/boot/bootctl-status.c @@ -0,0 +1,829 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/mman.h> +#include <unistd.h> + +#include "bootctl.h" +#include "bootctl-status.h" +#include "bootctl-util.h" +#include "bootspec.h" +#include "chase.h" +#include "devnum-util.h" +#include "dirent-util.h" +#include "efi-api.h" +#include "efi-loader.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "find-esp.h" +#include "path-util.h" +#include "pretty-print.h" +#include "recurse-dir.h" +#include "terminal-util.h" +#include "tpm2-util.h" + +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_NEG_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)); + if (settle_entry_token() >= 0) + printf("\n token: %s", arg_entry_token); + 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 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 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_and_opendir(path, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &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, *filename = NULL; + _cleanup_close_ int fd = -EBADF; + + if (!endswith_no_case(de->d_name, ".efi")) + continue; + + if (prefix && !startswith_no_case(de->d_name, prefix)) + continue; + + filename = path_join(p, de->d_name); + if (!filename) + return log_oom(); + LOG_SET_PREFIX(filename); + + fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open file for reading: %m"); + + r = get_file_version(fd, &v); + + if (r < 0 && r != -ESRCH) + 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 == -ESRCH) /* No systemd-owned file but still interesting to print */ + r = asprintf(previous, "/%s/%s", path, de->d_name); + else /* if (r >= 0) */ + r = asprintf(previous, "/%s/%s (%s%s%s)", path, de->d_name, ansi_highlight(), v, ansi_normal()); + 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) + goto fail; + + k = enumerate_binaries(esp_path, "EFI/BOOT", "boot", &last, &is_first); + if (k < 0) { + r = k; + goto fail; + } + + 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; + +fail: + errno = -r; + printf(" File: (can't access %s: %m)\n\n", esp_path); + return r; +} + +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); +} + +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= */ -1, /* 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); + return 0; + } + + r = acquire_xbootldr(/* unprivileged_mode= */ -1, &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); + 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" }, + { EFI_LOADER_FEATURE_SECUREBOOT_ENROLL, "Enroll SecureBoot keys" }, + { EFI_LOADER_FEATURE_RETAIN_SHIM, "Retain SHIM protocols" }, + { EFI_LOADER_FEATURE_MENU_DISABLE, "Menu can be disabled" }, + }; + 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" }, + { EFI_STUB_FEATURE_RANDOM_SEED, "Support for passing random seed to OS" }, + { EFI_STUB_FEATURE_CMDLINE_ADDONS, "Pick up .cmdline from addons" }, + { EFI_STUB_FEATURE_CMDLINE_SMBIOS, "Pick up .cmdline from SMBIOS Type 11" }, + { EFI_STUB_FEATURE_DEVICETREE_ADDONS, "Pick up .dtb from addons" }, + }; + _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: %s%s%s", + IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED) ? ansi_highlight_green() : ansi_normal(), + enabled_disabled(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)), + ansi_normal()); + + if (secure != SECURE_BOOT_DISABLED) + printf(" (%s)\n", secure_boot_mode_to_string(secure)); + else + printf("\n"); + + 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_measured_uki(LOG_DEBUG); + if (k > 0) + printf(" Measured UKI: %syes%s\n", ansi_highlight_green(), ansi_normal()); + else if (k == 0) + printf(" Measured UKI: no\n"); + else { + errno = -k; + printf(" Measured UKI: %sfailed%s (%m)\n", ansi_highlight_red(), 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(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) + RET_GATHER(r, status_binaries(arg_esp_path, esp_uuid)); + + if (!arg_root && is_efi_boot()) + RET_GATHER(r, status_variables()); + + 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); + RET_GATHER(r, k); + + if (k >= 0) + RET_GATHER(r, + status_entries(&config, + arg_esp_path, esp_uuid, + arg_xbootldr_path, xbootldr_uuid)); + } + + return r; +} + +static int ref_file(Hashmap *known_files, const char *fn, int increment) { + char *k = NULL; + int n, r; + + assert(known_files); + + /* just gracefully ignore this. This way the caller doesn't + have to verify whether the bootloader entry is relevant */ + if (!fn) + return 0; + + n = PTR_TO_INT(hashmap_get2(known_files, fn, (void**)&k)); + n += increment; + + assert(n >= 0); + + if (n == 0) { + (void) hashmap_remove(known_files, fn); + free(k); + } else if (!k) { + _cleanup_free_ char *t = NULL; + + t = strdup(fn); + if (!t) + return -ENOMEM; + r = hashmap_put(known_files, t, INT_TO_PTR(n)); + if (r < 0) + return r; + TAKE_PTR(t); + } else { + r = hashmap_update(known_files, fn, INT_TO_PTR(n)); + if (r < 0) + return r; + } + + return n; +} + +static void deref_unlink_file(Hashmap *known_files, const char *fn, const char *root) { + _cleanup_free_ char *path = NULL; + int r; + + assert(known_files); + + /* just gracefully ignore this. This way the caller doesn't + have to verify whether the bootloader entry is relevant */ + if (!fn || !root) + return; + + r = ref_file(known_files, fn, -1); + if (r < 0) + return (void) log_warning_errno(r, "Failed to deref \"%s\", ignoring: %m", fn); + if (r > 0) + return; + + if (arg_dry_run) { + r = chase_and_access(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, &path); + if (r < 0) + log_info_errno(r, "Unable to determine whether \"%s\" exists, ignoring: %m", fn); + else + log_info("Would remove \"%s\"", path); + return; + } + + r = chase_and_unlink(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, 0, &path); + if (r >= 0) + log_info("Removed \"%s\"", path); + else if (r != -ENOENT) + return (void) log_warning_errno(r, "Failed to remove \"%s\", ignoring: %m", fn); + + _cleanup_free_ char *d = NULL; + if (path_extract_directory(fn, &d) >= 0 && !path_equal(d, "/")) { + r = chase_and_unlink(d, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, AT_REMOVEDIR, NULL); + if (r < 0 && !IN_SET(r, -ENOTEMPTY, -ENOENT)) + log_warning_errno(r, "Failed to remove directory \"%s\", ignoring: %m", d); + } +} + +static int count_known_files(const BootConfig *config, const char* root, Hashmap **ret_known_files) { + _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL; + int r; + + assert(config); + assert(ret_known_files); + + known_files = hashmap_new(&path_hash_ops); + if (!known_files) + return -ENOMEM; + + for (size_t i = 0; i < config->n_entries; i++) { + const BootEntry *e = config->entries + i; + + if (!path_equal(e->root, root)) + continue; + + r = ref_file(known_files, e->kernel, +1); + if (r < 0) + return r; + r = ref_file(known_files, e->efi, +1); + if (r < 0) + return r; + STRV_FOREACH(s, e->initrd) { + r = ref_file(known_files, *s, +1); + if (r < 0) + return r; + } + r = ref_file(known_files, e->device_tree, +1); + if (r < 0) + return r; + STRV_FOREACH(s, e->device_tree_overlay) { + r = ref_file(known_files, *s, +1); + if (r < 0) + return r; + } + } + + *ret_known_files = TAKE_PTR(known_files); + + return 0; +} + +static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) { + assert(config); + + if (!root || !id) + return -ENOENT; + + for (size_t i = 0; i < config->n_entries; i++) + if (path_equal(config->entries[i].root, root) && + fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0) + return i; + + return -ENOENT; +} + +static int unlink_entry(const BootConfig *config, const char *root, const char *id) { + _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL; + const BootEntry *e = NULL; + int r; + + assert(config); + + r = count_known_files(config, root, &known_files); + if (r < 0) + return log_error_errno(r, "Failed to count files in %s: %m", root); + + r = boot_config_find_in(config, root, id); + if (r < 0) + return r; + + if (r == config->default_entry) + log_warning("%s is the default boot entry", id); + if (r == config->selected_entry) + log_warning("%s is the selected boot entry", id); + + e = &config->entries[r]; + + deref_unlink_file(known_files, e->kernel, e->root); + deref_unlink_file(known_files, e->efi, e->root); + STRV_FOREACH(s, e->initrd) + deref_unlink_file(known_files, *s, e->root); + deref_unlink_file(known_files, e->device_tree, e->root); + STRV_FOREACH(s, e->device_tree_overlay) + deref_unlink_file(known_files, *s, e->root); + + if (arg_dry_run) + log_info("Would remove \"%s\"", e->path); + else { + r = chase_and_unlink(e->path, root, CHASE_PROHIBIT_SYMLINKS, 0, NULL); + if (r < 0) + return log_error_errno(r, "Failed to remove \"%s\": %m", e->path); + + log_info("Removed %s", e->path); + } + + return 0; +} + +static int list_remove_orphaned_file( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + Hashmap *known_files = userdata; + + assert(path); + assert(known_files); + + if (event != RECURSE_DIR_ENTRY) + return RECURSE_DIR_CONTINUE; + + if (hashmap_get(known_files, path)) + return RECURSE_DIR_CONTINUE; /* keep! */ + + if (arg_dry_run) + log_info("Would remove %s", path); + else if (unlinkat(dir_fd, de->d_name, 0) < 0) + log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", path); + else + log_info("Removed %s", path); + + return RECURSE_DIR_CONTINUE; +} + +static int cleanup_orphaned_files( + const BootConfig *config, + const char *root) { + + _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL; + _cleanup_free_ char *full = NULL, *p = NULL; + _cleanup_close_ int dir_fd = -EBADF; + int r; + + assert(config); + assert(root); + + log_info("Cleaning %s", root); + + r = settle_entry_token(); + if (r < 0) + return r; + + r = count_known_files(config, root, &known_files); + if (r < 0) + return log_error_errno(r, "Failed to count files in %s: %m", root); + + dir_fd = chase_and_open(arg_entry_token, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, + O_DIRECTORY|O_CLOEXEC, &full); + if (dir_fd == -ENOENT) + return 0; + if (dir_fd < 0) + return log_error_errno(dir_fd, "Failed to open '%s/%s': %m", root, arg_entry_token); + + p = path_join("/", arg_entry_token); + if (!p) + return log_oom(); + + r = recurse_dir(dir_fd, p, 0, UINT_MAX, RECURSE_DIR_SORT, list_remove_orphaned_file, known_files); + if (r < 0) + return log_error_errno(r, "Failed to cleanup %s: %m", full); + + return r; +} + +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= */ -1, /* 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= */ -1, 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; + } + + if (streq(argv[0], "list")) { + pager_open(arg_pager_flags); + return show_boot_entries(&config, arg_json_format_flags); + } else if (streq(argv[0], "cleanup")) { + if (arg_xbootldr_path && xbootldr_devid != esp_devid) + cleanup_orphaned_files(&config, arg_xbootldr_path); + return cleanup_orphaned_files(&config, arg_esp_path); + } else { + assert(streq(argv[0], "unlink")); + if (arg_xbootldr_path && xbootldr_devid != esp_devid) { + r = unlink_entry(&config, arg_xbootldr_path, argv[1]); + if (r == 0 || r != -ENOENT) + return r; + } + return unlink_entry(&config, arg_esp_path, argv[1]); + } +} + +int verb_unlink(int argc, char *argv[], void *userdata) { + return verb_list(argc, argv, userdata); +} diff --git a/src/boot/bootctl-status.h b/src/boot/bootctl-status.h new file mode 100644 index 0000000..f7998a3 --- /dev/null +++ b/src/boot/bootctl-status.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +int verb_status(int argc, char *argv[], void *userdata); +int verb_list(int argc, char *argv[], void *userdata); +int verb_unlink(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-systemd-efi-options.c b/src/boot/bootctl-systemd-efi-options.c new file mode 100644 index 0000000..7f8308f --- /dev/null +++ b/src/boot/bootctl-systemd-efi-options.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bootctl.h" +#include "bootctl-systemd-efi-options.h" +#include "efi-loader.h" + +int verb_systemd_efi_options(int argc, char *argv[], void *userdata) { + int r; + + /* This is obsolete and subject to removal */ + + if (!arg_quiet) + log_notice("Use of the SystemdOptions EFI variable is deprecated."); + + 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; +} diff --git a/src/boot/bootctl-systemd-efi-options.h b/src/boot/bootctl-systemd-efi-options.h new file mode 100644 index 0000000..d0243eb --- /dev/null +++ b/src/boot/bootctl-systemd-efi-options.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_systemd_efi_options(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-uki.c b/src/boot/bootctl-uki.c new file mode 100644 index 0000000..8808c30 --- /dev/null +++ b/src/boot/bootctl-uki.c @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> + +#include "alloc-util.h" +#include "bootctl-uki.h" +#include "kernel-image.h" + +int verb_kernel_identify(int argc, char *argv[], void *userdata) { + KernelImageType t; + int r; + + r = inspect_kernel(AT_FDCWD, argv[1], &t, NULL, NULL, NULL); + if (r < 0) + return r; + + puts(kernel_image_type_to_string(t)); + return 0; +} + +int verb_kernel_inspect(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *cmdline = NULL, *uname = NULL, *pname = NULL; + KernelImageType t; + int r; + + r = inspect_kernel(AT_FDCWD, argv[1], &t, &cmdline, &uname, &pname); + if (r < 0) + return r; + + printf("Kernel Type: %s\n", kernel_image_type_to_string(t)); + if (cmdline) + printf(" Cmdline: %s\n", cmdline); + if (uname) + printf(" Version: %s\n", uname); + if (pname) + printf(" OS: %s\n", pname); + + return 0; +} diff --git a/src/boot/bootctl-uki.h b/src/boot/bootctl-uki.h new file mode 100644 index 0000000..effb984 --- /dev/null +++ b/src/boot/bootctl-uki.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +int verb_kernel_identify(int argc, char *argv[], void *userdata); +int verb_kernel_inspect(int argc, char *argv[], void *userdata); diff --git a/src/boot/bootctl-util.c b/src/boot/bootctl-util.c new file mode 100644 index 0000000..3cab875 --- /dev/null +++ b/src/boot/bootctl-util.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/mman.h> + +#include "bootctl.h" +#include "bootctl-util.h" +#include "errno-util.h" +#include "fileio.h" +#include "stat-util.h" +#include "sync-util.h" + +int sync_everything(void) { + int r = 0, k; + + if (arg_esp_path) { + k = syncfs_path(AT_FDCWD, arg_esp_path); + if (k < 0) + RET_GATHER(r, 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_GATHER(r, log_error_errno(k, "Failed to synchronize $BOOT '%s': %m", arg_xbootldr_path)); + } + + return r; +} + +const char *get_efi_arch(void) { + /* Detect EFI firmware architecture of the running system. On mixed mode systems, it could be 32-bit + * while the kernel is running in 64-bit. */ + +#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; +} + +/* search for "#### LoaderInfo: systemd-boot 218 ####" string inside the binary */ +int get_file_version(int fd, char **ret) { + struct stat st; + char *buf; + const char *s, *e; + char *marker = NULL; + int r; + + assert(fd >= 0); + assert(ret); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat EFI binary: %m"); + + r = stat_verify_regular(&st); + if (r < 0) { + log_debug_errno(r, "EFI binary is not a regular file, assuming no version information: %m"); + return -ESRCH; + } + + if (st.st_size < 27 || file_offset_beyond_memory_size(st.st_size)) + return log_debug_errno(SYNTHETIC_ERRNO(ESRCH), + "EFI binary size too %s: %"PRIi64, + st.st_size < 27 ? "small" : "large", st.st_size); + + buf = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (buf == MAP_FAILED) + return log_error_errno(errno, "Failed to mmap EFI binary: %m"); + + s = mempmem_safe(buf, st.st_size - 8, "#### LoaderInfo: ", 17); + if (!s) { + r = log_debug_errno(SYNTHETIC_ERRNO(ESRCH), "EFI binary has no LoaderInfo marker."); + goto finish; + } + + e = memmem_safe(s, st.st_size - (s - buf), " ####", 5); + if (!e || e - s < 3) { + r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "EFI binary has malformed LoaderInfo marker."); + goto finish; + } + + marker = strndup(s, e - s); + if (!marker) { + r = log_oom(); + goto finish; + } + + log_debug("EFI binary LoaderInfo marker: \"%s\"", marker); + r = 0; + *ret = marker; +finish: + (void) munmap(buf, st.st_size); + return r; +} + +int settle_entry_token(void) { + int r; + + r = boot_entry_token_ensure( + arg_root, + etc_kernel(), + arg_machine_id, + /* machine_id_is_random = */ false, + &arg_entry_token_type, + &arg_entry_token); + if (r < 0) + return r; + + log_debug("Using entry token: %s", arg_entry_token); + return 0; +} diff --git a/src/boot/bootctl-util.h b/src/boot/bootctl-util.h new file mode 100644 index 0000000..147455e --- /dev/null +++ b/src/boot/bootctl-util.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int sync_everything(void); + +const char *get_efi_arch(void); + +int get_file_version(int fd, char **ret); + +int settle_entry_token(void); + +static inline const char* etc_kernel(void) { + return getenv("KERNEL_INSTALL_CONF_ROOT") ?: "/etc/kernel/"; +} diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c new file mode 100644 index 0000000..4614ca1 --- /dev/null +++ b/src/boot/bootctl.c @@ -0,0 +1,516 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> + +#include "blockdev-util.h" +#include "bootctl.h" +#include "bootctl-install.h" +#include "bootctl-random-seed.h" +#include "bootctl-reboot-to-firmware.h" +#include "bootctl-set-efivar.h" +#include "bootctl-status.h" +#include "bootctl-systemd-efi-options.h" +#include "bootctl-uki.h" +#include "build.h" +#include "devnum-util.h" +#include "dissect-image.h" +#include "escape.h" +#include "find-esp.h" +#include "main-func.h" +#include "mount-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "utf8.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) + +char *arg_esp_path = NULL; +char *arg_xbootldr_path = NULL; +bool arg_print_esp_path = false; +bool arg_print_dollar_boot_path = false; +unsigned arg_print_root_device = 0; +bool arg_touch_variables = true; +PagerFlags arg_pager_flags = 0; +bool arg_graceful = false; +bool arg_quiet = false; +int arg_make_entry_directory = false; /* tri-state: < 0 for automatic logic */ +sd_id128_t arg_machine_id = SD_ID128_NULL; +char *arg_install_layout = NULL; +BootEntryTokenType arg_entry_token_type = BOOT_ENTRY_TOKEN_AUTO; +char *arg_entry_token = NULL; +JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +bool arg_arch_all = false; +char *arg_root = NULL; +char *arg_image = NULL; +InstallSource arg_install_source = ARG_INSTALL_SOURCE_AUTO; +char *arg_efi_boot_option_description = NULL; +bool arg_dry_run = false; +ImagePolicy *arg_image_policy = 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_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); + +int acquire_esp( + int 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; +} + +int acquire_xbootldr( + int 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 help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + pager_open(arg_pager_flags); + + 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" + "\n%3$sBoot Loader Specification Commands:%4$s\n" + " list List boot loader entries\n" + " unlink ID Remove boot loader entry\n" + " cleanup Remove files in ESP not referenced in any boot entry\n" + "\n%3$sBoot Loader Interface Commands:%4$s\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 or refresh random seed in ESP and EFI\n" + " variables\n" + "\n%3$sKernel Image Commands:%4$s\n" + " kernel-identify Identify kernel image type\n" + " kernel-inspect Prints details about the kernel image\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" + " --image-policy=POLICY\n" + " Specify disk image dissection policy\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 mount point\n" + " -x --print-boot-path Print path to the $BOOT partition mount point\n" + " -R --print-root-device\n" + " Print path to the block device node backing the\n" + " root file system (returns e.g. /dev/nvme0n1p5)\n" + " -RR Print path to the whole disk block device node\n" + " backing the root FS (returns e.g. /dev/nvme0n1)\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" + " --dry-run Dry run (unlink and cleanup)\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_IMAGE_POLICY, + 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, + ARG_DRY_RUN, + }; + + 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 }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "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' }, + { "print-root-device", no_argument, NULL, 'R' }, + { "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 }, + { "dry-run", no_argument, NULL, ARG_DRY_RUN }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hpxRq", 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_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + 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': + arg_print_esp_path = true; + break; + + case 'x': + arg_print_dollar_boot_path = true; + break; + + case 'R': + arg_print_root_device ++; + 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: + r = parse_boot_entry_token_type(optarg, &arg_entry_token_type, &arg_entry_token); + if (r < 0) + return r; + 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, NULL); + if (r < 0) + return r; + + arg_make_entry_directory = r; + } + 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 ARG_DRY_RUN: + arg_dry_run = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (!!arg_print_esp_path + !!arg_print_dollar_boot_path + (arg_print_root_device > 0) > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--print-esp-path/-p, --print-boot-path/-x, --print-root-device=/-R cannot be combined."); + + if ((arg_root || arg_image) && argv[optind] && !STR_IN_SET(argv[optind], "status", "list", + "install", "update", "remove", "is-installed", "random-seed", "unlink", "cleanup")) + 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=."); + + if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry is only supported with --unlink or --cleanup"); + + return 1; +} + +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 }, + { "kernel-identify", 2, 2, 0, verb_kernel_identify }, + { "kernel-inspect", 2, 2, 0, verb_kernel_inspect }, + { "list", VERB_ANY, 1, 0, verb_list }, + { "unlink", 2, 2, 0, verb_unlink }, + { "cleanup", 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_freep) char *mounted_dir = NULL; + int r; + + log_setup(); + + /* 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; + + if (arg_print_root_device > 0) { + _cleanup_free_ char *path = NULL; + dev_t devno; + + r = blockdev_get_root(LOG_ERR, &devno); + if (r < 0) + return r; + if (r == 0) { + log_error("Root file system not backed by a (single) whole block device."); + return 80; /* some recognizable error code */ + } + + if (arg_print_root_device > 1) { + r = block_get_whole_disk(devno, &devno); + if (r < 0) + log_debug_errno(r, "Unable to find whole block device for root block device, ignoring: %m"); + } + + r = device_path_make_canonical(S_IFBLK, devno, &path); + if (r < 0) + return log_error_errno(r, + "Failed to format canonical device path for devno '" DEVNUM_FORMAT_STR "': %m", + DEVNUM_FORMAT_VAL(devno)); + + puts(path); + return 0; + } + + /* Open up and mount the image */ + if (arg_image) { + assert(!arg_root); + + r = mount_image_privately_interactively( + arg_image, + arg_image_policy, + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK, + &mounted_dir, + /* ret_dir_fd= */ NULL, + &loop_device); + if (r < 0) + return r; + + arg_root = strdup(mounted_dir); + if (!arg_root) + return log_oom(); + } + + return bootctl_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/boot/bootctl.h b/src/boot/bootctl.h new file mode 100644 index 0000000..e395b33 --- /dev/null +++ b/src/boot/bootctl.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-id128.h" + +#include "boot-entry.h" +#include "image-policy.h" +#include "json.h" +#include "pager.h" + +typedef enum InstallSource { + ARG_INSTALL_SOURCE_IMAGE, + ARG_INSTALL_SOURCE_HOST, + ARG_INSTALL_SOURCE_AUTO, +} InstallSource; + +extern char *arg_esp_path; +extern char *arg_xbootldr_path; +extern bool arg_print_esp_path; +extern bool arg_print_dollar_boot_path; +extern unsigned arg_print_root_device; +extern bool arg_touch_variables; +extern PagerFlags arg_pager_flags; +extern bool arg_graceful; +extern bool arg_quiet; +extern int arg_make_entry_directory; /* tri-state: < 0 for automatic logic */ +extern sd_id128_t arg_machine_id; +extern char *arg_install_layout; +extern BootEntryTokenType arg_entry_token_type; +extern char *arg_entry_token; +extern JsonFormatFlags arg_json_format_flags; +extern bool arg_arch_all; +extern char *arg_root; +extern char *arg_image; +extern InstallSource arg_install_source; +extern char *arg_efi_boot_option_description; +extern bool arg_dry_run; +extern ImagePolicy *arg_image_policy; + +static inline 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; +} + +int acquire_esp(int 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); +int acquire_xbootldr(int unprivileged_mode, sd_id128_t *ret_uuid, dev_t *ret_devid); diff --git a/src/boot/efi/UEFI_SECURITY.md b/src/boot/efi/UEFI_SECURITY.md new file mode 100644 index 0000000..9f750d8 --- /dev/null +++ b/src/boot/efi/UEFI_SECURITY.md @@ -0,0 +1,122 @@ +# UEFI Components Security Posture +The systemd project provides a UEFI boot menu, `systemd-boot`, and a stub that can wrap a Linux kernel in a +PE binary, adding various features, `systemd-stub`. These components fully support UEFI SecureBoot, and +this document will describe their security posture and how they comply with industry-standard expectations +for UEFI SecureBoot workflows. + +Note that `systemd-stub` is not the same, or an alternative, to the Linux kernel's own EFI stub. The kernel +stub's role is that of the fundamental entrypoint to kernel execution from UEFI mode, implementing the +modern Linux boot protocol. `systemd-stub` on the other hand loads various resources, including the kernel +image, via the EFI LoadImage/StartImage protocol (although it does support the legacy Linux boot protocol, +as a fallback for older kernels on x86). The purpose of `systemd-stub` is to provide additional features and +functionality for either or both `systemd-boot` and `systemd` (userspace). + +## Fundamental Security Design Goals +The fundamental security design goals for these components are separation of security policy logic from the +rest of the functionality, achieved by offloading security-critical tasks to the firmware or earlier stages +of the boot process (e.g.: `Shim`). + +When SecureBoot is enabled, these components are designed to avoid executing, loading or using +unauthenticated payloads that could compromise the boot process, with special care taken for anything that +could affect the system before `ExitBootServices()` has been called. For example, when additional resources +are loaded, if running with SecureBoot enabled, they will be validated before use. The only exceptions are +the bootloader's own textual configuration files, and parsing metadata out of images for displaying purposes +only. There are no build time or runtime configuration options that can be set to weaken the security model +of these components when SecureBoot is enabled. + +The role of `systemd-boot` is to discover next stage components in the ESP (and XBOOTLDR if present), via +filesystem enumeration or explicit configuration files, and present a menu to the user, to choose the next +step. This auto discovery mechanism is described in details in the [BLS (Boot Loader +Specification)](https://uapi-group.org/specifications/specs/boot_loader_specification/). + +The role of `systemd-stub` is to load and measure in the TPM the post-bootloader stages, such as the kernel, +initrd and kernel command line, and implement optional features such as augmenting the initrd with +additional content such as configuration or optional services. [Unified Kernel +Images](https://uapi-group.org/specifications/specs/unified_kernel_image/) embed `systemd-stub`, a kernel +and other optional components as sections in a PE signed binary, that can thus be executed in UEFI +environments. + +Since it is embedded in a PE signed binary, `systemd-stub` will temporarily disable the UEFI authentication +protocol while loading the payload kernel it wraps, in order to avoid redundant duplicate authentication of +the image, given that the payload kernel was already authenticated and verified as part of the whole image. +SecureBoot authentication is re-enabled immediately after the kernel image has been loaded. + +Various EFI variables, under the vendor UUID `4a67b082-0a4c-41cf-b6c7-440b29bb8c4f`, are set and read by +these components, to pass metadata and configuration between different stages of the boot process, as +defined in the [Boot Loader Interface](https://systemd.io/BOOT_LOADER_INTERFACE/). + +## Dependencies +Neither of these components implements cryptographic primitives, cryptographic checks or drivers. File +access to the ESP is implemented solely via the appropriate UEFI file protocols. Verification of next stage +payloads is implementend solely via the appropriate UEFI image load protocols, which means authenticode +signature checks are again done by the firmware or `Shim`. As a consequence, no external security-critical +libraries (such as OpenSSL or gnu-efi) are used, linked or embedded. + +## Additional Resources +BLS Type #1 entries allow the user to load two types of additional resources that can affect the system +before `ExitBootServices()` has been called, kernel command line arguments and Devicetree blobs, that are +not validated before use, as they do not carry signatures. For this reason, when SecureBoot is enabled, +loading these resources is automatically disabled. There is no override for this security mechanism, neither +at build time nor at runtime. Note that initrds are also not verified in BLS Type #1 configurations, for +compatibility with how SecureBoot has been traditionally handled on Linux-based OSes, as the kernel will +only load them after `ExitBootServices()` has been called. + +Another mechanism is supported by `systemd-boot` and `systemd-stub` to add additional payloads to the boot +process: `addons`. Addons are PE signed binaries that can carry kernel command line arguments or Devicetree +blobs (more might be added in the future). In contrast to the user-specified additions in the Type #1 case +described above, these addons are loaded through the UEFI image loading protocol, and thus are subject to +signature validation, and will be rejected if not signed or if the signature is invalid, following the +standard SecureBoot model. They are also measured in the TPM. + +`systemd-boot` will also load file system drivers that are stored in the ESP, to allow enhancing the +firmware's capabilities. These are again PE signed binaries and will be verified using the appropriate +UEFI protocol. + +A random seed will be loaded and passed to the kernel for early-boot entropy pool filling if found in the +ESP. It is mixed with various other sources of entropy available in the UEFI environment, such as the RNG +protocol, the boot counter and the clock. Moreover, the seed is updated before the kernel is invoked, as +well as after the kernel is invoked (from userspace), with a new seed derived from the Linux kernel entropy +pool. + +When operating as a virtual machine payload, the loaded payloads can be customized via `SMBIOS Type 11 +Strings`, if the hypervisor specifies them. This is automatically disabled if running inside a confidential +computing VM. + +## Certificates Enrollment +When SecureBoot is supported but in `setup` mode, `systemd-boot` can enroll user certificates if a set of +`PK`, `KEK` and `db` certificates is found in the ESP, after which SecureBoot is enabled and a firmware +reset is performed. When running on bare metal, the certificate(s) will be shown to the user on the console, +and manual confirmation will be asked before proceeding. When running as a virtual machine payload, +enrollment is fully automated, without user interaction, unless disabled via a configuration file in the +ESP. The configuration file can also be used to disable enrollment completely. + +## Compiler Hardening +The PE binaries are built with `-fstack-protector-strong`, and the stack canary is seeded with random data if +the UEFI RNG protocol is available. + +The binaries also are linked with `-z relro` and ship with native PE relocations, with the conversion from +ELF performed at build time, instead of containing ELF dynamic relocations, so the image loaded by +firmware/Shim requires fewer writable pages. + +The binaries are linked by default with full LTO support, so no code will be shipped unless it's reachable. + +Finally, the binaries ship with the `NX_COMPAT` bit set. + +The CI infrastructure also employs fuzz testing on various components, including string functions and the +BCD parser. + +## SBAT +`systemd-boot` and `systemd-stub` are built with an `SBAT` section by default. There are build options to +allow customizations of the metadata included in the section, that can be used by downstream distributors. +The `systemd` project will participate in the coordinated `SBAT` disclosure and metadata revision process as +deemed necessary, in coordination with the Shim Review group. + +The upstream project name used to be unified (`systemd`) for both components, but since version v255 has +been split into separate `systemd-boot` and `systemd-stub` project names, so that each component can be +revisioned independently. Most of the code tend to be shared between these two components, but there is no +complete overlap, so it is possible for a vulnerability to affect only one component but not the other. + +## Known Vulnerabilities +There is currently one known (and fixed) security vulnerability affecting `systemd-boot` on arm64 and +riscv64 systems. For details of the affected and fixed versions, please see the [published security +advisory.](https://github.com/systemd/systemd/security/advisories/GHSA-6m6p-rjcq-334c) diff --git a/src/boot/efi/addon.c b/src/boot/efi/addon.c new file mode 100644 index 0000000..95b29da --- /dev/null +++ b/src/boot/efi/addon.c @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "efi.h" +#include "version.h" + +/* Magic string for recognizing our own binaries */ +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-addon " GIT_VERSION " ####"); + +/* This is intended to carry data, not to be executed */ + +EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table); +EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { + return EFI_UNSUPPORTED; +} diff --git a/src/boot/efi/bcd.c b/src/boot/efi/bcd.c new file mode 100644 index 0000000..4533d47 --- /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 use case 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 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..bb12d89 --- /dev/null +++ b/src/boot/efi/bcd.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.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..5c0f0ab --- /dev/null +++ b/src/boot/efi/boot.c @@ -0,0 +1,2748 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bcd.h" +#include "bootspec-fundamental.h" +#include "console.h" +#include "device-path-util.h" +#include "devicetree.h" +#include "drivers.h" +#include "efivars-fundamental.h" +#include "graphics.h" +#include "initrd.h" +#include "linux.h" +#include "measure.h" +#include "part-discovery.h" +#include "pe.h" +#include "proto/block-io.h" +#include "proto/device-path.h" +#include "proto/simple-text-io.h" +#include "random-seed.h" +#include "sbat.h" +#include "secure-boot.h" +#include "shim.h" +#include "ticks.h" +#include "tpm2-pcr.h" +#include "util.h" +#include "version.h" +#include "vmm.h" + +/* Magic string for recognizing our own binaries */ +#define SD_MAGIC "#### LoaderInfo: systemd-boot " GIT_VERSION " ####" +DECLARE_NOALLOC_SECTION(".sdmagic", SD_MAGIC); + +/* Makes systemd-boot available from \EFI\Linux\ for testing purposes. */ +DECLARE_NOALLOC_SECTION( + ".osrel", + "ID=systemd-boot\n" + "VERSION=\"" GIT_VERSION "\"\n" + "NAME=\"systemd-boot " GIT_VERSION "\"\n"); + +DECLARE_SBAT(SBAT_BOOT_SECTION_TEXT); + +typedef enum LoaderType { + 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, + _LOADER_TYPE_MAX, +} LoaderType; + +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; + LoaderType type; + char16_t *loader; + char16_t *devicetree; + char16_t *options; + bool options_implied; /* If true, these options are implied if we invoke the PE binary without any parameters (as in: UKI). If false we must specify these options explicitly. */ + 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; +} BootEntry; + +typedef struct { + BootEntry **entries; + size_t n_entries; + size_t idx_default; + size_t idx_default_efivar; + uint64_t timeout_sec; /* Actual timeout used (efi_main() override > efivar > config). */ + uint64_t timeout_sec_config; + uint64_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 auto_poweroff; + bool auto_reboot; + 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; +} 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 ↔ … + * + * Note: all the values below are ABI, so they are not allowed to change. The bootctl tool sets the numerical + * value of TIMEOUT_MENU_FORCE and TIMEOUT_MENU_HIDDEN, instead of the string for compatibility reasons. + * + * The other values may be set by systemd-boot itself and changing those will lead to functional regression + * when new version of systemd-boot is installed. + * + * All the 64bit values are not ABI and will never be written to an efi variable. + */ +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, + TIMEOUT_MENU_DISABLED = (uint64_t)UINT32_MAX + 1U, + TIMEOUT_TYPE_MAX64 = UINT64_MAX, +}; + +enum { + IDX_MAX = INT16_MAX, + IDX_INVALID, +}; + +static void cursor_left(size_t *cursor, size_t *first) { + assert(cursor); + assert(first); + + if ((*cursor) > 0) + (*cursor)--; + else if ((*first) > 0) + (*first)--; +} + +static void cursor_right(size_t *cursor, size_t *first, size_t x_max, size_t 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, size_t x_max, size_t y_pos) { + _cleanup_free_ char16_t *line = NULL, *print = NULL; + size_t size, len, first = 0, cursor = 0, clear = 0; + + /* Edit the line and return true if it should be executed, false if not. */ + + 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; + size_t j, cursor_color = EFI_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 = EFI_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; + + size_t k; + for (k = first + cursor; k < len && line[k] == ' '; k++) + clear++; + for (; k < len && line[k] != ' '; k++) + clear++; + + for (size_t 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, '\b'): + /* 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 (size_t 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 (size_t 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, '\n'): + case KEYPRESS(0, 0, '\r'): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, '\r'): /* 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, '\b'): + if (len == 0) + continue; + if (first == 0 && cursor == 0) + continue; + for (size_t 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 (size_t 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 size_t entry_lookup_key(Config *config, size_t start, char16_t key) { + assert(config); + + if (key == 0) + return IDX_INVALID; + + /* select entry by number key */ + if (key >= '1' && key <= '9') { + size_t i = key - '0'; + if (i > config->n_entries) + i = config->n_entries; + return i-1; + } + + /* find matching key in boot entries */ + for (size_t i = start; i < config->n_entries; i++) + if (config->entries[i]->key == key) + return i; + + for (size_t i = 0; i < start; i++) + if (config->entries[i]->key == key) + return i; + + return IDX_INVALID; +} + +static char16_t* update_timeout_efivar(Config *config, bool inc) { + assert(config); + + switch (config->timeout_sec) { + case TIMEOUT_MAX: + config->timeout_sec = inc ? TIMEOUT_MAX : config->timeout_sec - 1; + break; + case TIMEOUT_UNSET: + config->timeout_sec = inc ? TIMEOUT_MENU_FORCE : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_DISABLED: + config->timeout_sec = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + case TIMEOUT_MENU_FORCE: + config->timeout_sec = inc ? TIMEOUT_MENU_HIDDEN : TIMEOUT_MENU_FORCE; + break; + case TIMEOUT_MENU_HIDDEN: + config->timeout_sec = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + default: + config->timeout_sec = config->timeout_sec + (inc ? 1 : -1); + } + + config->timeout_sec_efivar = config->timeout_sec; + + switch (config->timeout_sec) { + case TIMEOUT_UNSET: + return xstrdup16(u"Menu timeout defined by configuration file."); + case TIMEOUT_MENU_DISABLED: + assert_not_reached(); + case TIMEOUT_MENU_FORCE: + return xstrdup16(u"Timeout disabled, menu will always be shown."); + case TIMEOUT_MENU_HIDDEN: + return xstrdup16(u"Menu hidden. Hold down key at bootup to show menu."); + default: + return xasprintf("Menu timeout set to %u s.", (uint32_t)config->timeout_sec_efivar); + } +} + +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 *) u"─") == EFI_SUCCESS; + + return cache; +} + +static bool ps_continue(void) { + const char16_t *sep = unicode_supported() ? u"───" : u"---"; + printf("\n%ls Press any key to continue, ESC or q to quit. %ls\n\n", sep, sep); + + 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_timeout_status(const char *label, uint64_t t) { + switch (t) { + case TIMEOUT_UNSET: + return; + case TIMEOUT_MENU_DISABLED: + return (void) printf("%s: menu-disabled\n", label); + case TIMEOUT_MENU_FORCE: + return (void) printf("%s: menu-force\n", label); + case TIMEOUT_MENU_HIDDEN: + return (void) printf("%s: menu-hidden\n", label); + default: + return (void) printf("%s: %u s\n", label, (uint32_t)t); + } +} + +static void print_status(Config *config, char16_t *loaded_image_path) { + size_t 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(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", &device_part_uuid); + + printf(" systemd-boot version: " GIT_VERSION "\n"); + if (loaded_image_path) + printf(" loaded image: %ls\n", loaded_image_path); + if (device_part_uuid) + printf(" loader partition UUID: %ls\n", device_part_uuid); + printf(" architecture: " EFI_MACHINE_TYPE_NAME "\n"); + printf(" UEFI specification: %u.%02u\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + printf(" firmware vendor: %ls\n", ST->FirmwareVendor); + printf(" firmware version: %u.%02u\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + printf(" OS indications: %#" PRIx64 "\n", get_os_indications_supported()); + printf(" secure boot: %ls (%ls)\n", + yes_no(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)), + secure_boot_mode_to_string(secure)); + printf(" shim: %ls\n", yes_no(shim_loaded())); + printf(" TPM: %ls\n", yes_no(tpm_present())); + printf(" console mode: %i/%" PRIi64 " (%zux%zu @%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; + + print_timeout_status(" timeout (config)", config->timeout_sec_config); + print_timeout_status(" timeout (EFI var)", config->timeout_sec_efivar); + + if (config->entry_default_config) + printf(" default (config): %ls\n", config->entry_default_config); + if (config->entry_default_efivar) + printf(" default (EFI var): %ls\n", config->entry_default_efivar); + if (config->entry_oneshot) + printf(" default (one-shot): %ls\n", config->entry_oneshot); + if (config->entry_saved) + printf(" saved entry: %ls\n", config->entry_saved); + printf(" editor: %ls\n", yes_no(config->editor)); + printf(" auto-entries: %ls\n", yes_no(config->auto_entries)); + printf(" auto-firmware: %ls\n", yes_no(config->auto_firmware)); + printf(" auto-poweroff: %ls\n", yes_no(config->auto_poweroff)); + printf(" auto-reboot: %ls\n", yes_no(config->auto_reboot)); + printf(" beep: %ls\n", yes_no(config->beep)); + printf(" reboot-for-bitlocker: %ls\n", yes_no(config->reboot_for_bitlocker)); + + switch (config->secure_boot_enroll) { + case ENROLL_OFF: + printf(" secure-boot-enroll: off\n"); + break; + case ENROLL_MANUAL: + printf(" secure-boot-enroll: manual\n"); + break; + case ENROLL_IF_SAFE: + printf(" secure-boot-enroll: if-safe\n"); + break; + case ENROLL_FORCE: + printf(" secure-boot-enroll: force\n"); + break; + default: + assert_not_reached(); + } + + switch (config->console_mode) { + case CONSOLE_MODE_AUTO: + printf(" console-mode (config): auto\n"); + break; + case CONSOLE_MODE_KEEP: + printf(" console-mode (config): keep\n"); + break; + case CONSOLE_MODE_FIRMWARE_MAX: + printf(" console-mode (config): max\n"); + break; + default: + printf(" console-mode (config): %" PRIi64 "\n", config->console_mode); + break; + } + + /* EFI var console mode is always a concrete value or unset. */ + if (config->console_mode_efivar != CONSOLE_MODE_KEEP) + printf("console-mode (EFI var): %" PRIi64 "\n", config->console_mode_efivar); + + if (!ps_continue()) + return; + + for (size_t i = 0; i < config->n_entries; i++) { + BootEntry *entry = config->entries[i]; + EFI_DEVICE_PATH *dp = NULL; + _cleanup_free_ char16_t *dp_str = NULL; + + if (entry->device && + BS->HandleProtocol(entry->device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp) == + EFI_SUCCESS) + (void) device_path_to_str(dp, &dp_str); + + printf(" boot entry: %zu/%zu\n", i + 1, config->n_entries); + printf(" id: %ls\n", entry->id); + if (entry->title) + printf(" title: %ls\n", entry->title); + if (entry->title_show && !streq16(entry->title, entry->title_show)) + printf(" title show: %ls\n", entry->title_show); + if (entry->sort_key) + printf(" sort key: %ls\n", entry->sort_key); + if (entry->version) + printf(" version: %ls\n", entry->version); + if (entry->machine_id) + printf(" machine-id: %ls\n", entry->machine_id); + if (dp_str) + printf(" device: %ls\n", dp_str); + if (entry->loader) + printf(" loader: %ls\n", entry->loader); + STRV_FOREACH(initrd, entry->initrd) + printf(" initrd: %ls\n", *initrd); + if (entry->devicetree) + printf(" devicetree: %ls\n", entry->devicetree); + if (entry->options) + printf(" options: %ls\n", entry->options); + printf(" internal call: %ls\n", yes_no(!!entry->call)); + + printf("counting boots: %ls\n", yes_no(entry->tries_left >= 0)); + if (entry->tries_left >= 0) { + printf(" tries: %i left, %i done\n", entry->tries_left, entry->tries_done); + printf(" current path: %ls\\%ls\n", entry->path, entry->current_name); + printf(" next path: %ls\\%ls\n", entry->path, entry->next_name); + } + + if (!ps_continue()) + return; + } +} + +static EFI_STATUS set_reboot_into_firmware(void) { + uint64_t osind = 0; + EFI_STATUS err; + + (void) efivar_get_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndications", &osind); + osind |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI; + + err = efivar_set_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndications", osind, EFI_VARIABLE_NON_VOLATILE); + if (err != EFI_SUCCESS) + log_error_status(err, "Error setting OsIndications: %m"); + return err; +} + +_noreturn_ static EFI_STATUS poweroff_system(void) { + RT->ResetSystem(EfiResetShutdown, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} + +_noreturn_ static EFI_STATUS reboot_system(void) { + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} + +static EFI_STATUS reboot_into_firmware(void) { + EFI_STATUS err; + + err = set_reboot_into_firmware(); + if (err != EFI_SUCCESS) + return err; + + return reboot_system(); +} + +static bool menu_run( + Config *config, + BootEntry **chosen_entry, + char16_t *loaded_image_path) { + + assert(config); + assert(chosen_entry); + + EFI_STATUS err; + size_t visible_max = 0; + size_t idx_highlight = config->idx_default, idx_highlight_prev = 0; + size_t idx, idx_first = 0, idx_last = 0; + bool new_mode = true, clear = true; + bool refresh = true, highlight = false; + size_t x_start = 0, y_start = 0, y_status = 0, x_max, y_max; + _cleanup_(strv_freep) char16_t **lines = NULL; + _cleanup_free_ char16_t *clearline = NULL, *separator = NULL, *status = NULL; + uint64_t timeout_efivar_saved = config->timeout_sec_efivar; + uint32_t timeout_remain = config->timeout_sec == TIMEOUT_MENU_FORCE ? 0 : config->timeout_sec; + int64_t console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar; + size_t default_efivar_saved = config->idx_default_efivar; + + enum { + ACTION_CONTINUE, /* Continue with loop over user input */ + ACTION_FIRMWARE_SETUP, /* Ask for confirmation and reboot into firmware setup */ + ACTION_POWEROFF, /* Power off the machine */ + ACTION_REBOOT, /* Reboot the machine */ + ACTION_RUN, /* Execute a boot entry */ + ACTION_QUIT, /* Return to the firmware */ + } action = ACTION_CONTINUE; + + 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 */ + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u" "); + + 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_status(err, "Error switching console mode: %m"); + } + + size_t line_width = 0, entry_padding = 3; + while (IN_SET(action, ACTION_CONTINUE, ACTION_FIRMWARE_SETUP)) { + 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->n_entries <= visible_max || idx_highlight <= visible_max / 2) + idx_first = 0; + else if (idx_highlight >= config->n_entries - (visible_max / 2)) + idx_first = config->n_entries - 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 (size_t i = 0; i < config->n_entries; 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->n_entries < visible_max) + y_start = ((visible_max - config->n_entries) / 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->n_entries) + 1, y_max - 1); + + lines = strv_free(lines); + clearline = mfree(clearline); + separator = mfree(separator); + + /* menu entries title lines */ + lines = xnew(char16_t *, config->n_entries + 1); + + for (size_t i = 0; i < config->n_entries; i++) { + size_t 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 (size_t 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->n_entries] = NULL; + + clearline = xnew(char16_t, x_max + 1); + separator = xnew(char16_t, x_max + 1); + for (size_t 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 (size_t i = idx_first; i <= idx_last && i < config->n_entries; 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() ? u" ►" : u"=>"); + } + 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() ? u" ►" : u"=>"); + if (idx_highlight == config->idx_default_efivar) + print_at(x_start, + y_start + idx_highlight - idx_first, + COLOR_HIGHLIGHT, + unicode_supported() ? u" ►" : u"=>"); + highlight = false; + } + + if (timeout_remain > 0) { + free(status); + status = xasprintf("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. */ + size_t len = strnlen16(status, x_max - 1); + size_t 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) { + action = ACTION_RUN; + break; + } + + /* update status */ + continue; + } + if (err != EFI_SUCCESS) { + action = ACTION_RUN; + break; + } + + timeout_remain = 0; + + /* clear status after keystroke */ + status = mfree(status); + + idx_highlight_prev = idx_highlight; + + if (action == ACTION_FIRMWARE_SETUP) { + if (IN_SET(key, KEYPRESS(0, 0, '\r'), KEYPRESS(0, 0, '\n')) && + set_reboot_into_firmware() == EFI_SUCCESS) + break; + + /* Any key other than newline or a failed attempt cancel the request. */ + action = ACTION_CONTINUE; + 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->n_entries-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->n_entries-1) { + refresh = true; + idx_highlight = config->n_entries-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->n_entries-1) + idx_highlight = config->n_entries-1; + break; + + case KEYPRESS(0, 0, '\n'): + case KEYPRESS(0, 0, '\r'): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, '\r'): /* Teclast X98+ II firmware sends malformed events */ + case KEYPRESS(0, SCAN_RIGHT, 0): + action = ACTION_RUN; + 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 = xasprintf("(d)efault (t/T)imeout (e)dit (r/R)esolution (p)rint %s%s(h)elp", + config->auto_poweroff ? "" : "(O)ff ", + config->auto_reboot ? "" : "re(B)oot "); + break; + + case KEYPRESS(0, 0, 'Q'): + action = ACTION_QUIT; + 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, false); + break; + + case KEYPRESS(0, 0, '+'): + case KEYPRESS(0, 0, 't'): + status = update_timeout_efivar(config, 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); + if (line_edit(&config->entries[idx_highlight]->options, x_max - 2, y_status)) + action = ACTION_RUN; + print_at(1, y_status, COLOR_NORMAL, clearline + 2); + + /* The options string was now edited, hence we have to pass it to the invoked + * binary. */ + config->entries[idx_highlight]->options_implied = false; + break; + + case KEYPRESS(0, 0, 'v'): + status = xasprintf( + "systemd-boot " GIT_VERSION " (" EFI_MACHINE_TYPE_NAME "), " + "UEFI Specification %u.%02u, Vendor %ls %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')): + case 'L': /* only uppercase, do not conflict with lower-case 'l' which picks first Linux entry */ + clear = true; + break; + + case KEYPRESS(0, 0, 'r'): + err = console_set_mode(CONSOLE_MODE_NEXT); + if (err != EFI_SUCCESS) + status = xasprintf_status(err, "Error changing console mode: %m"); + else { + config->console_mode_efivar = ST->ConOut->Mode->Mode; + status = xasprintf( + "Console mode changed to %" PRIi64 ".", + 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 = xasprintf_status(err, "Error resetting console mode: %m"); + else + status = xasprintf( + "Console mode reset to %s default.", + config->console_mode == CONSOLE_MODE_KEEP ? + "firmware" : + "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)) { + action = ACTION_FIRMWARE_SETUP; + /* Let's make sure the user really wants to do this. */ + status = xstrdup16(u"Press Enter to reboot into firmware interface."); + } else + status = xstrdup16(u"Reboot into firmware interface not supported."); + break; + + case KEYPRESS(0, 0, 'O'): /* Only uppercase, so that it can't be hit so easily fat-fingered, + * but still works safely over serial. */ + action = ACTION_POWEROFF; + break; + + case KEYPRESS(0, 0, 'B'): /* ditto */ + action = ACTION_REBOOT; + 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; + } + + /* Update EFI vars after we left the menu to reduce NVRAM writes. */ + + if (default_efivar_saved != config->idx_default_efivar) + efivar_set(MAKE_GUID_PTR(LOADER), u"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_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigConsoleMode", EFI_VARIABLE_NON_VOLATILE); + else + efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"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_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_DISABLED: + assert_not_reached(); + case TIMEOUT_MENU_FORCE: + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", u"menu-force", EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_HIDDEN: + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", u"menu-hidden", EFI_VARIABLE_NON_VOLATILE); + break; + default: + assert(config->timeout_sec_efivar < UINT32_MAX); + efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", + config->timeout_sec_efivar, EFI_VARIABLE_NON_VOLATILE); + } + } + + switch (action) { + case ACTION_CONTINUE: + assert_not_reached(); + case ACTION_POWEROFF: + poweroff_system(); + case ACTION_REBOOT: + case ACTION_FIRMWARE_SETUP: + reboot_system(); + case ACTION_RUN: + case ACTION_QUIT: + break; + } + + *chosen_entry = config->entries[idx_highlight]; + clear_screen(COLOR_NORMAL); + return action == ACTION_RUN; +} + +static void config_add_entry(Config *config, BootEntry *entry) { + assert(config); + assert(entry); + + /* This is just for paranoia. */ + assert(config->n_entries < IDX_MAX); + + if ((config->n_entries & 15) == 0) { + config->entries = xrealloc( + config->entries, + sizeof(void *) * config->n_entries, + sizeof(void *) * (config->n_entries + 16)); + } + config->entries[config->n_entries++] = entry; +} + +static BootEntry* boot_entry_free(BootEntry *entry) { + if (!entry) + return NULL; + + 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); + + return mfree(entry); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(BootEntry *, boot_entry_free); + +static void config_defaults_load_from_file(Config *config, char *content) { + char *line; + size_t pos = 0; + char *key, *value; + + assert(config); + assert(content); + + while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) + if (streq8(key, "timeout")) { + if (streq8(value, "menu-disabled")) + config->timeout_sec_config = TIMEOUT_MENU_DISABLED; + else 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("Error parsing 'timeout' config option, ignoring: %s", + value); + continue; + } + config->timeout_sec_config = u; + } + config->timeout_sec = config->timeout_sec_config; + + } else if (streq8(key, "default")) { + if (value[0] == '@' && !strcaseeq8(value, "@saved")) { + log_error("Unsupported special entry identifier, ignoring: %s", value); + continue; + } + free(config->entry_default_config); + config->entry_default_config = xstr8_to_16(value); + + } else if (streq8(key, "editor")) { + if (!parse_boolean(value, &config->editor)) + log_error("Error parsing 'editor' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-entries")) { + if (!parse_boolean(value, &config->auto_entries)) + log_error("Error parsing 'auto-entries' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-firmware")) { + if (!parse_boolean(value, &config->auto_firmware)) + log_error("Error parsing 'auto-firmware' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-poweroff")) { + if (!parse_boolean(value, &config->auto_poweroff)) + log_error("Error parsing 'auto-poweroff' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-reboot")) { + if (!parse_boolean(value, &config->auto_reboot)) + log_error("Error parsing 'auto-reboot' config option, ignoring: %s", value); + + } else if (streq8(key, "beep")) { + if (!parse_boolean(value, &config->beep)) + log_error("Error parsing 'beep' config option, ignoring: %s", value); + + } else if (streq8(key, "reboot-for-bitlocker")) { + if (!parse_boolean(value, &config->reboot_for_bitlocker)) + log_error("Error parsing 'reboot-for-bitlocker' config option, ignoring: %s", + value); + + } else 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, "if-safe")) + config->secure_boot_enroll = ENROLL_IF_SAFE; + else if (streq8(value, "off")) + config->secure_boot_enroll = ENROLL_OFF; + else + log_error("Error parsing 'secure-boot-enroll' config option, ignoring: %s", + value); + + } else 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("Error parsing 'console-mode' config option, ignoring: %s", + value); + continue; + } + config->console_mode = u; + } + } +} + +static void boot_entry_parse_tries( + BootEntry *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 = xasprintf( + "%.*ls%" PRIu64 "-%" PRIu64 "%ls", + (int) prefix_len, + file, + LESS_BY(tries_left, 1u), + MIN(tries_done + 1, (uint64_t) INT_MAX), + suffix); +} + +static EFI_STATUS boot_entry_bump_counters(BootEntry *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; + size_t 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) + return log_error_status(err, "Error opening entry root path: %m"); + + old_path = xasprintf("%ls\\%ls", 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) + return log_error_status(err, "Error opening boot entry: %m"); + + err = get_file_info(handle, &file_info, &file_info_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting boot entry file info: %m"); + + /* And rename the file */ + strcpy16(file_info->FileName, entry->next_name); + err = handle->SetInfo(handle, MAKE_GUID_PTR(EFI_FILE_INFO), file_info_size, file_info); + if (err != EFI_SUCCESS) + return log_error_status( + err, "Failed to rename '%ls' to '%ls', ignoring: %m", old_path, entry->next_name); + + /* Flush everything to disk, just in case… */ + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error flushing boot entry file info: %m"); + + /* 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 = xasprintf("%ls\\%ls", entry->path, entry->next_name); + efivar_set(MAKE_GUID_PTR(LOADER), u"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 boot_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_(boot_entry_freep) BootEntry *entry = NULL; + char *line; + size_t 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(BootEntry, 1); + *entry = (BootEntry) { + .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); + + } else if (streq8(key, "sort-key")) { + free(entry->sort_key); + entry->sort_key = xstr8_to_16(value); + + } else if (streq8(key, "version")) { + free(entry->version); + entry->version = xstr8_to_16(value); + + } else if (streq8(key, "machine-id")) { + free(entry->machine_id); + entry->machine_id = xstr8_to_16(value); + + } else if (streq8(key, "linux")) { + free(entry->loader); + entry->type = LOADER_LINUX; + entry->loader = xstr8_to_path(value); + entry->key = 'l'; + + } else 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; + } + + } else 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; + } + + } else if (streq8(key, "devicetree")) { + free(entry->devicetree); + entry->devicetree = xstr8_to_path(value); + + } else 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; + + } else if (streq8(key, "options")) { + _cleanup_free_ char16_t *new = NULL; + + new = xstr8_to_16(value); + if (entry->options) { + char16_t *s = xasprintf("%ls %ls", entry->options, new); + free(entry->options); + entry->options = s; + } else + entry->options = TAKE_PTR(new); + } + + 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); + + boot_entry_parse_tries(entry, path, file, u".conf"); + TAKE_PTR(entry); +} + +static EFI_STATUS efivar_get_timeout(const char16_t *var, uint64_t *ret_value) { + _cleanup_free_ char16_t *value = NULL; + EFI_STATUS err; + + assert(var); + assert(ret_value); + + err = efivar_get(MAKE_GUID_PTR(LOADER), var, &value); + if (err != EFI_SUCCESS) + return err; + + if (streq16(value, u"menu-disabled")) { + *ret_value = TIMEOUT_MENU_DISABLED; + return EFI_SUCCESS; + } + 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; + size_t content_size, value = 0; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + assert(root_dir); + + *config = (Config) { + .editor = true, + .auto_entries = true, + .auto_firmware = true, + .secure_boot_enroll = ENROLL_IF_SAFE, + .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, u"\\loader\\loader.conf", 0, 0, &content, &content_size); + if (err == EFI_SUCCESS) { + /* First, measure. */ + err = tpm_log_tagged_event( + TPM2_PCR_BOOT_LOADER_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(content), + content_size, + LOADER_CONF_EVENT_TAG_ID, + u"loader.conf", + /* ret_measured= */ NULL); + if (err != EFI_SUCCESS) + log_error_status(err, "Error measuring loader.conf into TPM: %m"); + + /* Then: parse */ + 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_status(err, "Error reading LoaderConfigTimeout EFI variable: %m"); + + err = efivar_get_timeout(u"LoaderConfigTimeoutOneShot", &config->timeout_sec); + if (err == EFI_SUCCESS) { + /* Unset variable now, after all it's "one shot". */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeoutOneShot", EFI_VARIABLE_NON_VOLATILE); + + config->force_menu = true; /* force the menu when this is set */ + } else if (err != EFI_NOT_FOUND) + log_error_status(err, "Error reading LoaderConfigTimeoutOneShot EFI variable: %m"); + + err = efivar_get_uint_string(MAKE_GUID_PTR(LOADER), u"LoaderConfigConsoleMode", &value); + if (err == EFI_SUCCESS) + config->console_mode_efivar = value; + + err = efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderEntryOneShot", &config->entry_oneshot); + if (err == EFI_SUCCESS) + /* Unset variable now, after all it's "one shot". */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderEntryOneShot", EFI_VARIABLE_NON_VOLATILE); + + (void) efivar_get(MAKE_GUID_PTR(LOADER), u"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, u"@saved"); + config->use_saved_entry_efivar = streq16(config->entry_default_efivar, u"@saved"); + if (config->use_saved_entry || config->use_saved_entry_efivar) + (void) efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", &config->entry_saved); +} + +static void config_load_type1_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; + size_t 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, u"\\loader\\entries", &entries_dir); + if (err != EFI_SUCCESS) + return; + + for (;;) { + _cleanup_free_ char *content = NULL; + + err = readdir(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, u".conf")) + continue; + if (startswith(f->FileName, u"auto-")) + continue; + + err = file_read(entries_dir, f->FileName, 0, 0, &content, NULL); + if (err == EFI_SUCCESS) + boot_entry_add_type1(config, device, root_dir, u"\\loader\\entries", f->FileName, content, loaded_image_path); + } +} + +static int boot_entry_compare(const BootEntry *a, const BootEntry *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 size_t config_find_entry(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 (size_t i = 0; i < config->n_entries; i++) + if (efi_fnmatch(pattern, config->entries[i]->id)) + return i; + + return IDX_INVALID; +} + +static void config_select_default_entry(Config *config) { + size_t i; + + assert(config); + + i = config_find_entry(config, config->entry_oneshot); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + i = config_find_entry(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_find_entry(config, config->entry_saved); + else + i = config_find_entry(config, config->entry_default_config); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + /* select the first suitable entry */ + for (i = 0; i < config->n_entries; i++) + if (config->entries[i]->type != LOADER_AUTO && !config->entries[i]->call) { + 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(BootEntry **entries, bool *unique, size_t n_entries) { + bool is_unique = true; + + assert(entries); + assert(unique); + + for (size_t i = 0; i < n_entries; i++) + for (size_t k = i + 1; k < n_entries; k++) { + if (!streq16(entries[i]->title_show, entries[k]->title_show)) + continue; + + is_unique = unique[i] = unique[k] = false; + } + + return is_unique; +} + +/* generate unique titles, avoiding non-distinguishable menu entries */ +static void generate_boot_entry_titles(Config *config) { + assert(config); + + bool unique[config->n_entries]; + + /* set title */ + for (size_t i = 0; i < config->n_entries; 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->n_entries)) + return; + + /* add version to non-unique titles */ + for (size_t i = 0; i < config->n_entries; 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 = xasprintf("%ls (%ls)", t, config->entries[i]->version); + } + + if (entries_unique(config->entries, unique, config->n_entries)) + return; + + /* add machine-id to non-unique titles */ + for (size_t i = 0; i < config->n_entries; 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 = xasprintf("%ls (%.8ls)", t, config->entries[i]->machine_id); + } + + if (entries_unique(config->entries, unique, config->n_entries)) + return; + + /* add file name to non-unique titles */ + for (size_t i = 0; i < config->n_entries; i++) { + if (unique[i]) + continue; + + _cleanup_free_ char16_t *t = config->entries[i]->title_show; + config->entries[i]->title_show = xasprintf("%ls (%ls)", 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 + }; + size_t 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(SD_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, SD_MAGIC, sizeof(SD_MAGIC)) == 0; +} + +static BootEntry* config_add_entry_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 = u"\\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, u"\\EFI\\BOOT\\GRUB" EFI_MACHINE_TYPE_NAME u".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; + + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .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_add_entry_osx(Config *config) { + EFI_STATUS err; + size_t n_handles = 0; + _cleanup_free_ EFI_HANDLE *handles = NULL; + + assert(config); + + if (!config->auto_entries) + return; + + err = BS->LocateHandleBuffer( + ByProtocol, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return; + + for (size_t i = 0; i < n_handles; i++) { + _cleanup_(file_closep) EFI_FILE *root = NULL; + + if (open_volume(handles[i], &root) != EFI_SUCCESS) + continue; + + if (config_add_entry_loader_auto( + config, + handles[i], + root, + NULL, + u"auto-osx", + 'a', + u"macOS", + u"\\System\\Library\\CoreServices\\boot.efi")) + break; + } +} + +#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) +static EFI_STATUS boot_windows_bitlocker(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t 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, MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return err; + + /* Look for BitLocker magic string on all block drives. */ + bool found = false; + for (size_t i = 0; i < n_handles; i++) { + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->HandleProtocol(handles[i], MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), (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; + size_t 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(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"BootOrder", (char **) &boot_order, &boot_order_size); + if (err != EFI_SUCCESS || boot_order_size % sizeof(uint16_t) != 0) + return err; + + for (size_t i = 0; i < boot_order_size / sizeof(uint16_t); i++) { + _cleanup_free_ char *buf = NULL; + size_t buf_size; + + _cleanup_free_ char16_t *name = xasprintf("Boot%04x", boot_order[i]); + err = efivar_get_raw(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), 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. */ + size_t offset = sizeof(uint32_t) + sizeof(uint16_t); + if (buf_size < offset + sizeof(char16_t)) + continue; + + if (streq16((char16_t *) (buf + offset), u"Windows Boot Manager")) { + err = efivar_set_raw( + MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), + u"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; +} +#endif + +static void config_add_entry_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; + size_t len; + + assert(config); + assert(device); + assert(root_dir); + + if (!config->auto_entries) + return; + + /* Try to find a better title. */ + err = file_read(root_dir, u"\\EFI\\Microsoft\\Boot\\BCD", 0, 100*1024, &bcd, &len); + if (err == EFI_SUCCESS) + title = get_bcd_title((uint8_t *) bcd, len); + + BootEntry *e = config_add_entry_loader_auto(config, device, root_dir, NULL, + u"auto-windows", 'w', title ?: u"Windows Boot Manager", + u"\\EFI\\Microsoft\\Boot\\bootmgfw.efi"); + + if (config->reboot_for_bitlocker) + e->call = boot_windows_bitlocker; +#endif +} + +static void config_load_type2_entries( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir) { + + _cleanup_(file_closep) EFI_FILE *linux_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *f = NULL; + size_t 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, u"\\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; + size_t offs[_SECTION_MAX] = {}, szs[_SECTION_MAX] = {}, pos = 0; + char *line, *key, *value; + + err = readdir(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, u".efi")) + continue; + if (startswith(f->FileName, u"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); + + } else if (streq8(key, "IMAGE_ID")) { + free(os_image_id); + os_image_id = xstr8_to_16(value); + + } else if (streq8(key, "NAME")) { + free(os_name); + os_name = xstr8_to_16(value); + + } else if (streq8(key, "ID")) { + free(os_id); + os_id = xstr8_to_16(value); + + } else if (streq8(key, "IMAGE_VERSION")) { + free(os_image_version); + os_image_version = xstr8_to_16(value); + + } else if (streq8(key, "VERSION")) { + free(os_version); + os_version = xstr8_to_16(value); + + } else if (streq8(key, "VERSION_ID")) { + free(os_version_id); + os_version_id = xstr8_to_16(value); + + } else if (streq8(key, "BUILD_ID")) { + free(os_build_id); + os_build_id = xstr8_to_16(value); + } + + 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; + + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(f->FileName), + .type = LOADER_UNIFIED_LINUX, + .title = xstrdup16(good_name), + .version = xstrdup16(good_version), + .device = device, + .loader = xasprintf("\\EFI\\Linux\\%ls", f->FileName), + .sort_key = xstrdup16(good_sort_key), + .key = 'l', + .tries_done = -1, + .tries_left = -1, + }; + + strtolower16(entry->id); + config_add_entry(config, entry); + boot_entry_parse_tries(entry, u"\\EFI\\Linux", f->FileName, u".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); + entry->options_implied = true; + } + } +} + +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 = partition_open(MAKE_GUID_PTR(XBOOTLDR), device, &new_device, &root_dir); + if (err != EFI_SUCCESS) + return; + + config_load_type2_entries(config, new_device, root_dir); + config_load_type1_entries(config, new_device, root_dir, NULL); +} + +static EFI_STATUS initrd_prepare( + EFI_FILE *root, + const BootEntry *entry, + char16_t **ret_options, + void **ret_initrd, + size_t *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; + size_t size = 0; + _cleanup_free_ uint8_t *initrd = NULL; + + STRV_FOREACH(i, entry->initrd) { + _cleanup_free_ char16_t *o = options; + if (o) + options = xasprintf("%ls initrd=%ls", o, *i); + else + options = xasprintf("initrd=%ls", *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(handle, &info, NULL); + if (err != EFI_SUCCESS) + return err; + + if (info->FileSize == 0) /* Automatically skip over empty files */ + continue; + + size_t 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 = xasprintf("%ls %ls", 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 BootEntry *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(err, "Error opening root path: %m"); + + err = make_file_device_path(entry->device, entry->loader, &path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making file device path: %m"); + + size_t 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(err, "Error preparing initrd: %m"); + + err = shim_load_image(parent_image, path, &image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error loading %ls: %m", entry->loader); + + /* DTBs are loaded by the kernel before ExitBootServices, and they can be used to map and assign + * arbitrary memory ranges, so skip them 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(err, "Error loading %ls: %m", entry->devicetree); + } + + _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; + err = initrd_register(initrd, initrd_size, &initrd_handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error registering initrd: %m"); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting LoadedImageProtocol handle: %m"); + + /* If we had to append an initrd= entry to the command line, we have to pass it, and measure it. + * Otherwise, only pass/measure it if it is not implicit anyway (i.e. embedded into the UKI or + * so). */ + char16_t *options = options_initrd ?: entry->options_implied ? NULL : 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(MAKE_GUID_PTR(LOADER), u"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(err, "Error finding kernel compat entry address: %m"); + } 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(err, "Failed to execute %ls (%ls): %m", entry->title_show, entry->loader); +} + +static void config_free(Config *config) { + assert(config); + for (size_t i = 0; i < config->n_entries; i++) + boot_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; + size_t sz = 0; + char *p; + + assert(config); + + for (size_t i = 0; i < config->n_entries; i++) + sz += strsize16(config->entries[i]->id); + + p = buffer = xmalloc(sz); + + for (size_t i = 0; i < config->n_entries; 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(MAKE_GUID_PTR(LOADER), u"LoaderEntries", buffer, sz, 0); +} + +static void save_selected_entry(const Config *config, const BootEntry *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(MAKE_GUID_PTR(LOADER), u"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(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", entry->id, EFI_VARIABLE_NON_VOLATILE); + } else + /* Delete the non-volatile var if not needed. */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", 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; + BootEntry *entry = NULL; + + err = readdir(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(BootEntry, 1); + *entry = (BootEntry) { + .id = xasprintf("secure-boot-keys-%ls", dirent->FileName), + .title = xasprintf("Enroll Secure Boot keys: %ls", dirent->FileName), + .path = xasprintf("\\loader\\keys\\%ls", dirent->FileName), + .type = LOADER_SECURE_BOOT_KEYS, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + + if (IN_SET(config->secure_boot_enroll, ENROLL_IF_SAFE, 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, config->secure_boot_enroll == ENROLL_FORCE); + } + + 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 | + EFI_LOADER_FEATURE_SECUREBOOT_ENROLL | + EFI_LOADER_FEATURE_RETAIN_SHIM | + EFI_LOADER_FEATURE_MENU_DISABLE | + 0; + + _cleanup_free_ char16_t *infostr = NULL, *typestr = NULL; + + assert(loaded_image); + + efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeInitUSec", init_usec); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderInfo", u"systemd-boot " GIT_VERSION, 0); + + infostr = xasprintf("%ls %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", infostr, 0); + + typestr = xasprintf("UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareType", typestr, 0); + + (void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"LoaderFeatures", loader_features, 0); + + /* the filesystem path to this image, to prevent adding ourselves to the menu */ + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderImageIdentifier", loaded_image_path, 0); + + /* export the device path this image is started from */ + _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); + if (uuid) + efivar_set(MAKE_GUID_PTR(LOADER), u"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_load_type2_entries(config, loaded_image->DeviceHandle, root_dir); + + /* Scan /loader/entries/\*.conf files */ + config_load_type1_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->n_entries, (compare_pointer_func_t) boot_entry_compare); + + /* If we find some well-known loaders, add them to the end of the list */ + config_add_entry_osx(config); + config_add_entry_windows(config, loaded_image->DeviceHandle, root_dir); + config_add_entry_loader_auto(config, loaded_image->DeviceHandle, root_dir, NULL, + u"auto-efi-shell", 's', u"EFI Shell", u"\\shell" EFI_MACHINE_TYPE_NAME ".efi"); + config_add_entry_loader_auto(config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + u"auto-efi-default", '\0', u"EFI Default Loader", NULL); + + if (config->auto_firmware && FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .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); + } + + if (config->auto_poweroff) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(u"auto-poweroff"), + .title = xstrdup16(u"Power Off The System"), + .call = poweroff_system, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + } + + if (config->auto_reboot) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(u"auto-reboot"), + .title = xstrdup16(u"Reboot The System"), + .call = reboot_system, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + } + + /* Find secure boot signing keys and autoload them if configured. + * Otherwise, create 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->n_entries == 0) + return; + + config_write_entries_to_variable(config); + + generate_boot_entry_titles(config); + + /* Select entry by configured pattern or EFI LoaderDefaultEntry= variable */ + config_select_default_entry(config); +} + +static EFI_STATUS discover_root_dir(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, EFI_FILE **ret_dir) { + if (is_direct_boot(loaded_image->DeviceHandle)) + return vmm_open(&loaded_image->DeviceHandle, ret_dir); + else + return open_volume(loaded_image->DeviceHandle, ret_dir); +} + +static EFI_STATUS run(EFI_HANDLE image) { + 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; + + init_usec = time_usec(); + + /* Ask Shim to leave its protocol around, so that the stub can use it to validate PEs. + * By default, Shim uninstalls its protocol when calling StartImage(). */ + shim_retain_protocol(); + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); + + (void) device_path_to_str(loaded_image->FilePath, &loaded_image_path); + + export_variables(loaded_image, loaded_image_path, init_usec); + + err = discover_root_dir(loaded_image, &root_dir); + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + (void) load_drivers(image, loaded_image, root_dir); + + config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir); + + if (config.n_entries == 0) + return log_error_status( + EFI_NOT_FOUND, + "No loader found. Configuration files in \\loader\\entries\\*.conf are needed."); + + /* select entry or show menu when key is pressed or timeout is set */ + if (config.force_menu || !IN_SET(config.timeout_sec, TIMEOUT_MENU_HIDDEN, TIMEOUT_MENU_DISABLED)) + menu = true; + else if (config.timeout_sec != TIMEOUT_MENU_DISABLED) { + 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 boot entries */ + size_t idx = entry_lookup_key(&config, config.idx_default, KEYCHAR(key)); + if (idx != IDX_INVALID) + config.idx_default = idx; + else + menu = true; + } + } + + for (;;) { + BootEntry *entry; + + entry = config.entries[config.idx_default]; + if (menu) { + efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeMenuUSec", 0); + if (!menu_run(&config, &entry, loaded_image_path)) + return EFI_SUCCESS; + } + + /* 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, /*force=*/ true); + 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) boot_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); + + err = image_start(image, entry); + if (err != EFI_SUCCESS) + return err; + + menu = true; + config.timeout_sec = 0; + } +} + +DEFINE_EFI_MAIN_FUNCTION(run, "systemd-boot", /*wait_for_debugger=*/false); diff --git a/src/boot/efi/console.c b/src/boot/efi/console.c new file mode 100644 index 0000000..067ee7c --- /dev/null +++ b/src/boot/efi/console.c @@ -0,0 +1,312 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "console.h" +#include "proto/graphics-output.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 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; + size_t index; + EFI_STATUS err; + _cleanup_(event_closep) EFI_EVENT timer = NULL; + + assert(key); + + if (!checked) { + /* Get the *first* TextInputEx device. */ + err = BS->LocateProtocol( + MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL), 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, + MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL), + (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(err, "Error creating timer event: %m"); + + EFI_EVENT events[] = { + timer, + conInEx ? conInEx->WaitForKeyEx : ST->ConIn->WaitForKey, + extraInEx ? extraInEx->WaitForKeyEx : NULL, + }; + size_t 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(err, "Error arming timer event: %m"); + + (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(err, "Error waiting for events: %m"); + + /* 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 size_t, 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); + + log_wait(); + 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(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), 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; + size_t 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(size_t *x_max, size_t *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..c4d821a --- /dev/null +++ b/src/boot/efi/console.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/simple-text-io.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(size_t *x_max, size_t *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..5b90e17 --- /dev/null +++ b/src/boot/efi/cpio.c @@ -0,0 +1,512 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "device-path-util.h" +#include "measure.h" +#include "proto/device-path.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 (size_t 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, + size_t contents_size, + const char *target_dir_prefix, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + size_t 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 > 32-bit 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 > SIZE_MAX - target_dir_prefix_size) + return EFI_OUT_OF_RESOURCES; + l += target_dir_prefix_size; + + fname_size = strlen16(fname); + if (l > SIZE_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 == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + /* Align the contents to 4 byte size */ + q = ALIGN4(contents_size); + if (q == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (l > SIZE_MAX - q) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + l += q; /* Add contents to header */ + + if (*cpio_buffer_size > SIZE_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, + size_t *cpio_buffer_size) { + + size_t 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 > SIZE_MAX - path_size) + return EFI_OUT_OF_RESOURCES; + l += path_size; + + /* Align the whole header to 4 byte size */ + l = ALIGN4(l); + if (l == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (*cpio_buffer_size > SIZE_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, + size_t *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, + size_t *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; +} + +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, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured) { + + _cleanup_(file_closep) EFI_FILE *root = NULL, *extra_dir = NULL; + size_t 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(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(err, "Unable to open root directory: %m"); + + if (!dropin_dir) { + dropin_dir = rel_dropin_dir = get_extra_dir(loaded_image->FilePath); + if (!dropin_dir) + goto nothing; + } + + 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(err, "Failed to open extra directory of loaded image: %m"); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read extra directory of loaded image: %m"); + 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) { + /* We allocate 16 entries at a time, as a matter of optimization */ + if (n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + size_t 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(err, "Failed to pack cpio prefix: %m"); + + for (size_t i = 0; i < n_items; i++) { + _cleanup_free_ char *content = NULL; + size_t 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(err, "Failed to read %ls, ignoring: %m", items[i]); + 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(err, "Failed to pack cpio file %ls: %m", dirent->FileName); + } + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *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 = false; + + 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, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *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; + size_t buffer_size = 0; + EFI_STATUS err; + + assert(data || data_size == 0); + assert(target_dir_prefix); + assert(target_filename); + 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(err, "Failed to pack cpio prefix: %m"); + + 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(err, "Failed to pack cpio file %ls: %m", target_filename); + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *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..26851e3 --- /dev/null +++ b/src/boot/efi/cpio.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/loaded-image.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, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *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, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured); diff --git a/src/boot/efi/device-path-util.c b/src/boot/efi/device-path-util.c new file mode 100644 index 0000000..2a85e8b --- /dev/null +++ b/src/boot/efi/device-path-util.c @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "util.h" + +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, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *end_node = dp; + while (!device_path_is_end(end_node)) + end_node = device_path_next_node(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 + sizeof(FILEPATH_DEVICE_PATH) + sizeof(EFI_DEVICE_PATH)); + dp = mempcpy(*ret_dp, dp, dp_size); + + FILEPATH_DEVICE_PATH *file_dp = (FILEPATH_DEVICE_PATH *) dp; + file_dp->Header = (EFI_DEVICE_PATH) { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_FILEPATH_DP, + .Length = sizeof(FILEPATH_DEVICE_PATH) + file_size, + }; + memcpy(file_dp->PathName, file, file_size); + + dp = device_path_next_node(dp); + *dp = DEVICE_PATH_END_NODE; + return EFI_SUCCESS; +} + +static char16_t *device_path_to_str_internal(const EFI_DEVICE_PATH *dp) { + char16_t *str = NULL; + + for (const EFI_DEVICE_PATH *node = dp; !device_path_is_end(node); node = device_path_next_node(node)) { + _cleanup_free_ char16_t *old = str; + + if (node->Type == END_DEVICE_PATH_TYPE && node->SubType == END_INSTANCE_DEVICE_PATH_SUBTYPE) { + str = xasprintf("%ls%s,", strempty(old), old ? "\\" : ""); + continue; + } + + /* Special-case this so that FilePath-only device path string look and behave nicely. */ + if (node->Type == MEDIA_DEVICE_PATH && node->SubType == MEDIA_FILEPATH_DP) { + str = xasprintf("%ls%s%ls", + strempty(old), + old ? "\\" : "", + ((FILEPATH_DEVICE_PATH *) node)->PathName); + continue; + } + + /* Instead of coding all the different types and sub-types here we just use the + * generic node form. This function is a best-effort for firmware that does not + * provide the EFI_DEVICE_PATH_TO_TEXT_PROTOCOL after all. */ + + size_t size = node->Length - sizeof(EFI_DEVICE_PATH); + _cleanup_free_ char16_t *hex_data = hexdump((uint8_t *) node + sizeof(EFI_DEVICE_PATH), size); + str = xasprintf("%ls%sPath(%u,%u%s%ls)", + strempty(old), + old ? "/" : "", + node->Type, + node->SubType, + size == 0 ? "" : ",", + hex_data); + } + + return str; +} + +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(MAKE_GUID_PTR(EFI_DEVICE_PATH_TO_TEXT_PROTOCOL), NULL, (void **) &dp_to_text); + if (err != EFI_SUCCESS) { + *ret = device_path_to_str_internal(dp); + 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; +} + +bool device_path_startswith(const EFI_DEVICE_PATH *dp, const EFI_DEVICE_PATH *start) { + if (!start) + return true; + if (!dp) + return false; + for (;;) { + if (device_path_is_end(start)) + return true; + if (device_path_is_end(dp)) + return false; + if (start->Length != dp->Length) + return false; + if (memcmp(dp, start, start->Length) != 0) + return false; + start = device_path_next_node(start); + dp = device_path_next_node(dp); + } +} + +EFI_DEVICE_PATH *device_path_replace_node( + const EFI_DEVICE_PATH *path, const EFI_DEVICE_PATH *node, const EFI_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; + EFI_DEVICE_PATH *ret = xmalloc(len + (new_node ? new_node->Length : 0) + sizeof(EFI_DEVICE_PATH)); + EFI_DEVICE_PATH *end = mempcpy(ret, path, len); + + if (new_node) + end = mempcpy(end, new_node, new_node->Length); + + *end = DEVICE_PATH_END_NODE; + return ret; +} diff --git a/src/boot/efi/device-path-util.h b/src/boot/efi/device-path-util.h new file mode 100644 index 0000000..08f1a9c --- /dev/null +++ b/src/boot/efi/device-path-util.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "proto/device-path.h" + +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); +bool device_path_startswith(const EFI_DEVICE_PATH *dp, const EFI_DEVICE_PATH *start); +EFI_DEVICE_PATH *device_path_replace_node( + const EFI_DEVICE_PATH *path, const EFI_DEVICE_PATH *node, const EFI_DEVICE_PATH *new_node); + +static inline EFI_DEVICE_PATH *device_path_next_node(const EFI_DEVICE_PATH *dp) { + assert(dp); + return (EFI_DEVICE_PATH *) ((uint8_t *) dp + dp->Length); +} + +static inline bool device_path_is_end(const EFI_DEVICE_PATH *dp) { + assert(dp); + return dp->Type == END_DEVICE_PATH_TYPE && dp->SubType == END_ENTIRE_DEVICE_PATH_SUBTYPE; +} + +#define DEVICE_PATH_END_NODE \ + (EFI_DEVICE_PATH) { \ + .Type = END_DEVICE_PATH_TYPE, \ + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, \ + .Length = sizeof(EFI_DEVICE_PATH) \ + } diff --git a/src/boot/efi/devicetree.c b/src/boot/efi/devicetree.c new file mode 100644 index 0000000..61a43cd --- /dev/null +++ b/src/boot/efi/devicetree.c @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "devicetree.h" +#include "proto/dt-fixup.h" +#include "util.h" + +#define FDT_V1_SIZE (7*4) + +static EFI_STATUS devicetree_allocate(struct devicetree_state *state, size_t size) { + size_t 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 size_t devicetree_allocated(const struct devicetree_state *state) { + assert(state); + return state->pages * EFI_PAGE_SIZE; +} + +static EFI_STATUS devicetree_fixup(struct devicetree_state *state, size_t len) { + EFI_DT_FIXUP_PROTOCOL *fixup; + size_t size; + EFI_STATUS err; + + assert(state); + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_DT_FIXUP_PROTOCOL), NULL, (void **) &fixup); + /* Skip fixup if we cannot locate device tree fixup protocol */ + if (err != EFI_SUCCESS) + return EFI_SUCCESS; + + 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; + size_t 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; + size_t len; + EFI_STATUS err; + + assert(state); + assert(root_dir); + assert(name); + + /* Capture the original value for the devicetree table. NULL is not an error in this case so we don't + * need to check the return value. NULL simply means the system fw had no devicetree initially (and + * is the correct value to use to return to the initial state if needed). */ + state->orig = find_configuration_table(MAKE_GUID_PTR(EFI_DTB_TABLE)); + + 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(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( + MAKE_GUID_PTR(EFI_DTB_TABLE), PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +EFI_STATUS devicetree_install_from_memory( + struct devicetree_state *state, const void *dtb_buffer, size_t dtb_length) { + + EFI_STATUS err; + + assert(state); + assert(dtb_buffer && dtb_length > 0); + + /* Capture the original value for the devicetree table. NULL is not an error in this case so we don't + * need to check the return value. NULL simply means the system fw had no devicetree initially (and + * is the correct value to use to return to the initial state if needed). */ + state->orig = find_configuration_table(MAKE_GUID_PTR(EFI_DTB_TABLE)); + + 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( + MAKE_GUID_PTR(EFI_DTB_TABLE), PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +void devicetree_cleanup(struct devicetree_state *state) { + EFI_STATUS err; + + if (!state->pages) + return; + + err = BS->InstallConfigurationTable(MAKE_GUID_PTR(EFI_DTB_TABLE), 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..33eaa22 --- /dev/null +++ b/src/boot/efi/devicetree.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +struct devicetree_state { + EFI_PHYSICAL_ADDRESS addr; + size_t 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, size_t dtb_length); +void devicetree_cleanup(struct devicetree_state *state); diff --git a/src/boot/efi/drivers.c b/src/boot/efi/drivers.c new file mode 100644 index 0000000..0674557 --- /dev/null +++ b/src/boot/efi/drivers.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.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 = xasprintf("\\EFI\\systemd\\drivers\\%ls", fname); + err = make_file_device_path(loaded_image->DeviceHandle, spath, &path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making file device path: %m"); + + err = BS->LoadImage(false, parent_image, path, NULL, 0, &image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to load image %ls: %m", fname); + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to find protocol in driver image %ls: %m", fname); + + if (loaded_image->ImageCodeType != EfiBootServicesCode && + loaded_image->ImageCodeType != EfiRuntimeServicesCode) + return log_error("Image %ls 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_status(err, "Failed to start image %ls: %m", fname); + 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(err, "Failed to get list of handles: %m"); + + 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; + size_t dirent_size = 0, n_succeeded = 0; + EFI_STATUS err; + + err = open_directory( + root_dir, + u"\\EFI\\systemd\\drivers", + &drivers_dir); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open \\EFI\\systemd\\drivers: %m"); + + for (;;) { + err = readdir(drivers_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read extra directory of loaded image: %m"); + 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 u".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..ecd0b4e --- /dev/null +++ b/src/boot/efi/drivers.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/loaded-image.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..4144c0d --- /dev/null +++ b/src/boot/efi/efi-string.c @@ -0,0 +1,1084 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "efi-string.h" + +#if SD_BOOT +# include "proto/simple-text-io.h" +# 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 c ? NULL : (type *) s; \ + } + +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; +} + +char *startswith8(const char *s, const char *prefix) { + size_t l; + + assert(prefix); + + if (!s) + return NULL; + + l = strlen8(prefix); + if (!strneq8(s, prefix, l)) + return NULL; + + return (char*) s + l; +} + +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); + +bool parse_boolean(const char *v, bool *ret) { + assert(ret); + + if (!v) + return false; + + if (streq8(v, "1") || streq8(v, "yes") || streq8(v, "y") || streq8(v, "true") || streq8(v, "t") || + streq8(v, "on")) { + *ret = true; + return true; + } + + if (streq8(v, "0") || streq8(v, "no") || streq8(v, "n") || streq8(v, "false") || streq8(v, "f") || + streq8(v, "off")) { + *ret = false; + return true; + } + + return false; +} + +char *line_get_key_value(char *s, const char *sep, size_t *pos, char **ret_key, char **ret_value) { + char *line, *value; + size_t linelen; + + assert(s); + assert(sep); + assert(pos); + assert(ret_key); + assert(ret_value); + + for (;;) { + line = s + *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 (s[*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'; + } + + *ret_key = line; + *ret_value = value; + return line; + } +} + +char16_t *hexdump(const void *data, size_t size) { + static const char hex[16] = "0123456789abcdef"; + const uint8_t *d = data; + + assert(data || size == 0); + + char16_t *buf = xnew(char16_t, size * 2 + 1); + + for (size_t i = 0; i < size; i++) { + buf[i * 2] = hex[d[i] >> 4]; + buf[i * 2 + 1] = hex[d[i] & 0x0F]; + } + + buf[size * 2] = 0; + return buf; +} + +static const char * const warn_table[] = { + [EFI_SUCCESS] = "Success", + [EFI_WARN_UNKNOWN_GLYPH] = "Unknown glyph", + [EFI_WARN_DELETE_FAILURE] = "Delete failure", + [EFI_WARN_WRITE_FAILURE] = "Write failure", + [EFI_WARN_BUFFER_TOO_SMALL] = "Buffer too small", + [EFI_WARN_STALE_DATA] = "Stale data", + [EFI_WARN_FILE_SYSTEM] = "File system", + [EFI_WARN_RESET_REQUIRED] = "Reset required", +}; + +/* Errors have MSB set, remove it to keep the table compact. */ +#define NOERR(err) ((err) & ~EFI_ERROR_MASK) + +static const char * const err_table[] = { + [NOERR(EFI_ERROR_MASK)] = "Error", + [NOERR(EFI_LOAD_ERROR)] = "Load error", + [NOERR(EFI_INVALID_PARAMETER)] = "Invalid parameter", + [NOERR(EFI_UNSUPPORTED)] = "Unsupported", + [NOERR(EFI_BAD_BUFFER_SIZE)] = "Bad buffer size", + [NOERR(EFI_BUFFER_TOO_SMALL)] = "Buffer too small", + [NOERR(EFI_NOT_READY)] = "Not ready", + [NOERR(EFI_DEVICE_ERROR)] = "Device error", + [NOERR(EFI_WRITE_PROTECTED)] = "Write protected", + [NOERR(EFI_OUT_OF_RESOURCES)] = "Out of resources", + [NOERR(EFI_VOLUME_CORRUPTED)] = "Volume corrupt", + [NOERR(EFI_VOLUME_FULL)] = "Volume full", + [NOERR(EFI_NO_MEDIA)] = "No media", + [NOERR(EFI_MEDIA_CHANGED)] = "Media changed", + [NOERR(EFI_NOT_FOUND)] = "Not found", + [NOERR(EFI_ACCESS_DENIED)] = "Access denied", + [NOERR(EFI_NO_RESPONSE)] = "No response", + [NOERR(EFI_NO_MAPPING)] = "No mapping", + [NOERR(EFI_TIMEOUT)] = "Time out", + [NOERR(EFI_NOT_STARTED)] = "Not started", + [NOERR(EFI_ALREADY_STARTED)] = "Already started", + [NOERR(EFI_ABORTED)] = "Aborted", + [NOERR(EFI_ICMP_ERROR)] = "ICMP error", + [NOERR(EFI_TFTP_ERROR)] = "TFTP error", + [NOERR(EFI_PROTOCOL_ERROR)] = "Protocol error", + [NOERR(EFI_INCOMPATIBLE_VERSION)] = "Incompatible version", + [NOERR(EFI_SECURITY_VIOLATION)] = "Security violation", + [NOERR(EFI_CRC_ERROR)] = "CRC error", + [NOERR(EFI_END_OF_MEDIA)] = "End of media", + [NOERR(EFI_ERROR_RESERVED_29)] = "Reserved (29)", + [NOERR(EFI_ERROR_RESERVED_30)] = "Reserved (30)", + [NOERR(EFI_END_OF_FILE)] = "End of file", + [NOERR(EFI_INVALID_LANGUAGE)] = "Invalid language", + [NOERR(EFI_COMPROMISED_DATA)] = "Compromised data", + [NOERR(EFI_IP_ADDRESS_CONFLICT)] = "IP address conflict", + [NOERR(EFI_HTTP_ERROR)] = "HTTP error", +}; + +static const char *status_to_string(EFI_STATUS status) { + if (status <= ELEMENTSOF(warn_table) - 1) + return warn_table[status]; + if (status >= EFI_ERROR_MASK && status <= ((ELEMENTSOF(err_table) - 1) | EFI_ERROR_MASK)) + return err_table[NOERR(status)]; + return NULL; +} + +typedef struct { + size_t padded_len; /* Field width in printf. */ + size_t len; /* Precision in printf. */ + bool pad_zero; + bool align_left; + bool alternative_form; + bool long_arg; + bool longlong_arg; + bool have_field_width; + + const char *str; + const wchar_t *wstr; + + /* For numbers. */ + bool is_signed; + bool lowercase; + int8_t base; + char sign_pad; /* For + and (space) flags. */ +} SpecifierContext; + +typedef struct { + char16_t stack_buf[128]; /* We use stack_buf first to avoid allocations in most cases. */ + char16_t *dyn_buf; /* Allocated buf or NULL if stack_buf is used. */ + char16_t *buf; /* Points to the current active buf. */ + size_t n_buf; /* Len of buf (in char16_t's, not bytes!). */ + size_t n; /* Used len of buf (in char16_t's). This is always <n_buf. */ + + EFI_STATUS status; + const char *format; + va_list ap; +} FormatContext; + +static void grow_buf(FormatContext *ctx, size_t need) { + assert(ctx); + + assert_se(!__builtin_add_overflow(ctx->n, need, &need)); + + if (need < ctx->n_buf) + return; + + /* Greedily allocate if we can. */ + if (__builtin_mul_overflow(need, 2, &ctx->n_buf)) + ctx->n_buf = need; + + /* We cannot use realloc here as ctx->buf may be ctx->stack_buf, which we cannot free. */ + char16_t *new_buf = xnew(char16_t, ctx->n_buf); + memcpy(new_buf, ctx->buf, ctx->n * sizeof(*ctx->buf)); + + free(ctx->dyn_buf); + ctx->buf = ctx->dyn_buf = new_buf; +} + +static void push_padding(FormatContext *ctx, char pad, size_t len) { + assert(ctx); + while (len > 0) { + len--; + ctx->buf[ctx->n++] = pad; + } +} + +static bool push_str(FormatContext *ctx, SpecifierContext *sp) { + assert(ctx); + assert(sp); + + sp->padded_len = LESS_BY(sp->padded_len, sp->len); + + grow_buf(ctx, sp->padded_len + sp->len); + + if (!sp->align_left) + push_padding(ctx, ' ', sp->padded_len); + + /* In userspace unit tests we cannot just memcpy() the wide string. */ + if (sp->wstr && sizeof(wchar_t) == sizeof(char16_t)) { + memcpy(ctx->buf + ctx->n, sp->wstr, sp->len * sizeof(*sp->wstr)); + ctx->n += sp->len; + } else { + assert(sp->str || sp->wstr); + for (size_t i = 0; i < sp->len; i++) + ctx->buf[ctx->n++] = sp->str ? sp->str[i] : sp->wstr[i]; + } + + if (sp->align_left) + push_padding(ctx, ' ', sp->padded_len); + + assert(ctx->n < ctx->n_buf); + return true; +} + +static bool push_num(FormatContext *ctx, SpecifierContext *sp, uint64_t u) { + const char *digits = sp->lowercase ? "0123456789abcdef" : "0123456789ABCDEF"; + char16_t tmp[32]; + size_t n = 0; + + assert(ctx); + assert(sp); + assert(IN_SET(sp->base, 10, 16)); + + /* "%.0u" prints nothing if value is 0. */ + if (u == 0 && sp->len == 0) + return true; + + if (sp->is_signed && (int64_t) u < 0) { + /* We cannot just do "u = -(int64_t)u" here because -INT64_MIN overflows. */ + + uint64_t rem = -((int64_t) u % sp->base); + u = (int64_t) u / -sp->base; + tmp[n++] = digits[rem]; + sp->sign_pad = '-'; + } + + while (u > 0 || n == 0) { + uint64_t rem = u % sp->base; + u /= sp->base; + tmp[n++] = digits[rem]; + } + + /* Note that numbers never get truncated! */ + size_t prefix = (sp->sign_pad != 0 ? 1 : 0) + (sp->alternative_form ? 2 : 0); + size_t number_len = prefix + MAX(n, sp->len); + grow_buf(ctx, MAX(sp->padded_len, number_len)); + + size_t padding = 0; + if (sp->pad_zero) + /* Leading zeroes go after the sign or 0x prefix. */ + number_len = MAX(number_len, sp->padded_len); + else + padding = LESS_BY(sp->padded_len, number_len); + + if (!sp->align_left) + push_padding(ctx, ' ', padding); + + if (sp->sign_pad != 0) + ctx->buf[ctx->n++] = sp->sign_pad; + if (sp->alternative_form) { + ctx->buf[ctx->n++] = '0'; + ctx->buf[ctx->n++] = sp->lowercase ? 'x' : 'X'; + } + + push_padding(ctx, '0', LESS_BY(number_len, n + prefix)); + + while (n > 0) + ctx->buf[ctx->n++] = tmp[--n]; + + if (sp->align_left) + push_padding(ctx, ' ', padding); + + assert(ctx->n < ctx->n_buf); + return true; +} + +/* This helps unit testing. */ +#if SD_BOOT +# define NULLSTR "(null)" +# define wcsnlen strnlen16 +#else +# define NULLSTR "(nil)" +#endif + +static bool handle_format_specifier(FormatContext *ctx, SpecifierContext *sp) { + /* Parses one item from the format specifier in ctx and put the info into sp. If we are done with + * this specifier returns true, otherwise this function should be called again. */ + + /* This implementation assumes 32-bit ints. Also note that all types smaller than int are promoted to + * int in vararg functions, which is why we fetch only ints for any such types. The compiler would + * otherwise warn about fetching smaller types. */ + assert_cc(sizeof(int) == 4); + assert_cc(sizeof(wchar_t) <= sizeof(int)); + assert_cc(sizeof(intmax_t) <= sizeof(long long)); + + assert(ctx); + assert(sp); + + switch (*ctx->format) { + case '#': + sp->alternative_form = true; + return false; + case '.': + sp->have_field_width = true; + return false; + case '-': + sp->align_left = true; + return false; + case '+': + case ' ': + sp->sign_pad = *ctx->format; + return false; + + case '0': + if (!sp->have_field_width) { + sp->pad_zero = true; + return false; + } + + /* If field width has already been provided then 0 is part of precision (%.0s). */ + _fallthrough_; + + case '*': + case '1' ... '9': { + int64_t i; + + if (*ctx->format == '*') + i = va_arg(ctx->ap, int); + else { + uint64_t u; + if (!parse_number8(ctx->format, &u, &ctx->format) || u > INT_MAX) + assert_not_reached(); + ctx->format--; /* Point it back to the last digit. */ + i = u; + } + + if (sp->have_field_width) { + /* Negative precision is ignored. */ + if (i >= 0) + sp->len = (size_t) i; + } else { + /* Negative field width is treated as positive field width with '-' flag. */ + if (i < 0) { + i *= -1; + sp->align_left = true; + } + sp->padded_len = i; + } + + return false; + } + + case 'h': + if (*(ctx->format + 1) == 'h') + ctx->format++; + /* char/short gets promoted to int, nothing to do here. */ + return false; + + case 'l': + if (*(ctx->format + 1) == 'l') { + ctx->format++; + sp->longlong_arg = true; + } else + sp->long_arg = true; + return false; + + case 'z': + sp->long_arg = sizeof(size_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(size_t) == sizeof(long long); + return false; + + case 'j': + sp->long_arg = sizeof(intmax_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(intmax_t) == sizeof(long long); + return false; + + case 't': + sp->long_arg = sizeof(ptrdiff_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(ptrdiff_t) == sizeof(long long); + return false; + + case '%': + sp->str = "%"; + sp->len = 1; + return push_str(ctx, sp); + + case 'c': + sp->wstr = &(wchar_t){ va_arg(ctx->ap, int) }; + sp->len = 1; + return push_str(ctx, sp); + + case 's': + if (sp->long_arg) { + sp->wstr = va_arg(ctx->ap, const wchar_t *) ?: L"(null)"; + sp->len = wcsnlen(sp->wstr, sp->len); + } else { + sp->str = va_arg(ctx->ap, const char *) ?: "(null)"; + sp->len = strnlen8(sp->str, sp->len); + } + return push_str(ctx, sp); + + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + sp->lowercase = *ctx->format == 'x'; + sp->is_signed = IN_SET(*ctx->format, 'd', 'i'); + sp->base = IN_SET(*ctx->format, 'x', 'X') ? 16 : 10; + if (sp->len == SIZE_MAX) + sp->len = 1; + + uint64_t v; + if (sp->longlong_arg) + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, long long) : + va_arg(ctx->ap, unsigned long long); + else if (sp->long_arg) + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, long) : va_arg(ctx->ap, unsigned long); + else + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, int) : va_arg(ctx->ap, unsigned); + + return push_num(ctx, sp, v); + + case 'p': { + const void *ptr = va_arg(ctx->ap, const void *); + if (!ptr) { + sp->str = NULLSTR; + sp->len = STRLEN(NULLSTR); + return push_str(ctx, sp); + } + + sp->base = 16; + sp->lowercase = true; + sp->alternative_form = true; + sp->len = 0; /* Precision is ignored for %p. */ + return push_num(ctx, sp, (uintptr_t) ptr); + } + + case 'm': { + sp->str = status_to_string(ctx->status); + if (sp->str) { + sp->len = strlen8(sp->str); + return push_str(ctx, sp); + } + + sp->base = 16; + sp->lowercase = true; + sp->alternative_form = true; + sp->len = 0; + return push_num(ctx, sp, ctx->status); + } + + default: + assert_not_reached(); + } +} + +/* printf_internal is largely compatible to userspace vasprintf. Any features omitted should trigger asserts. + * + * Supported: + * - Flags: #, 0, +, -, space + * - Lengths: h, hh, l, ll, z, j, t + * - Specifiers: %, c, s, u, i, d, x, X, p, m + * - Precision and width (inline or as int arg using *) + * + * Notable differences: + * - Passing NULL to %s is permitted and will print "(null)" + * - %p will also use "(null)" + * - The provided EFI_STATUS is used for %m instead of errno + * - "\n" is translated to "\r\n" */ +_printf_(2, 0) static char16_t *printf_internal(EFI_STATUS status, const char *format, va_list ap, bool ret) { + assert(format); + + FormatContext ctx = { + .buf = ctx.stack_buf, + .n_buf = ELEMENTSOF(ctx.stack_buf), + .format = format, + .status = status, + }; + + /* We cannot put this into the struct without making a copy. */ + va_copy(ctx.ap, ap); + + while (*ctx.format != '\0') { + SpecifierContext sp = { .len = SIZE_MAX }; + + switch (*ctx.format) { + case '%': + ctx.format++; + while (!handle_format_specifier(&ctx, &sp)) + ctx.format++; + ctx.format++; + break; + case '\n': + ctx.format++; + sp.str = "\r\n"; + sp.len = 2; + push_str(&ctx, &sp); + break; + default: + sp.str = ctx.format++; + while (!IN_SET(*ctx.format, '%', '\n', '\0')) + ctx.format++; + sp.len = ctx.format - sp.str; + push_str(&ctx, &sp); + } + } + + va_end(ctx.ap); + + assert(ctx.n < ctx.n_buf); + ctx.buf[ctx.n++] = '\0'; + + if (ret) { + if (ctx.dyn_buf) + return TAKE_PTR(ctx.dyn_buf); + + char16_t *ret_buf = xnew(char16_t, ctx.n); + memcpy(ret_buf, ctx.buf, ctx.n * sizeof(*ctx.buf)); + return ret_buf; + } + +#if SD_BOOT + ST->ConOut->OutputString(ST->ConOut, ctx.buf); +#endif + + return mfree(ctx.dyn_buf); +} + +void printf_status(EFI_STATUS status, const char *format, ...) { + va_list ap; + va_start(ap, format); + printf_internal(status, format, ap, false); + va_end(ap); +} + +void vprintf_status(EFI_STATUS status, const char *format, va_list ap) { + printf_internal(status, format, ap, false); +} + +char16_t *xasprintf_status(EFI_STATUS status, const char *format, ...) { + va_list ap; + va_start(ap, format); + char16_t *ret = printf_internal(status, format, ap, true); + va_end(ap); + return ret; +} + +char16_t *xvasprintf_status(EFI_STATUS status, const char *format, va_list ap) { + return printf_internal(status, format, ap, true); +} + +#if SD_BOOT +/* To provide the actual implementation for these we need to remove the redirection to the builtins. */ +# undef memchr +# undef memcmp +# undef memcpy +# undef memset +_used_ void *memchr(const void *p, int c, size_t n); +_used_ int memcmp(const void *p1, const void *p2, size_t n); +_used_ void *memcpy(void * restrict dest, const void * restrict src, size_t n); +_used_ void *memset(void *p, int c, size_t n); +#else +/* And for userspace unit testing we need to give them an efi_ prefix. */ +# define memchr efi_memchr +# define memcmp efi_memcmp +# define memcpy efi_memcpy +# define memset efi_memset +#endif + +void *memchr(const void *p, int c, size_t n) { + if (!p || n == 0) + return NULL; + + const uint8_t *q = p; + for (size_t i = 0; i < n; i++) + if (q[i] == (unsigned char) c) + return (void *) (q + i); + + return NULL; +} + +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; +} + +void *memcpy(void * restrict dest, const void * restrict src, size_t n) { + if (!dest || !src || n == 0) + return dest; + +#if 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; +} + +void *memset(void *p, int c, size_t n) { + if (!p || n == 0) + return p; + +#if 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..9ac919f --- /dev/null +++ b/src/boot/efi/efi-string.h @@ -0,0 +1,180 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.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)); +} + +char *startswith8(const char *s, const char *prefix); + +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); + +bool parse_boolean(const char *v, bool *ret); + +char *line_get_key_value(char *s, const char *sep, size_t *pos, char **ret_key, char **ret_value); + +char16_t *hexdump(const void *data, size_t size); + +#ifdef __clang__ +# define _gnu_printf_(a, b) _printf_(a, b) +#else +# define _gnu_printf_(a, b) __attribute__((format(gnu_printf, a, b))) +#endif + +_gnu_printf_(2, 3) void printf_status(EFI_STATUS status, const char *format, ...); +_gnu_printf_(2, 0) void vprintf_status(EFI_STATUS status, const char *format, va_list ap); +_gnu_printf_(2, 3) _warn_unused_result_ char16_t *xasprintf_status(EFI_STATUS status, const char *format, ...); +_gnu_printf_(2, 0) _warn_unused_result_ char16_t *xvasprintf_status(EFI_STATUS status, const char *format, va_list ap); + +#if SD_BOOT +# define printf(...) printf_status(EFI_SUCCESS, __VA_ARGS__) +# define xasprintf(...) xasprintf_status(EFI_SUCCESS, __VA_ARGS__) + +/* inttypes.h is provided by libc instead of the compiler and is not supposed to be used in freestanding + * environments. We could use clang __*_FMT*__ constants for this, bug gcc does not have them. :( */ + +# if defined(__ILP32__) || defined(__arm__) || defined(__i386__) +# define PRI64_PREFIX "ll" +# elif defined(__LP64__) +# define PRI64_PREFIX "l" +# elif defined(__LLP64__) || (__SIZEOF_LONG__ == 4 && __SIZEOF_POINTER__ == 8) +# define PRI64_PREFIX "ll" +# else +# error Unknown 64-bit data model +# endif + +# define PRIi32 "i" +# define PRIu32 "u" +# define PRIx32 "x" +# define PRIX32 "X" +# define PRIiPTR "zi" +# define PRIuPTR "zu" +# define PRIxPTR "zx" +# define PRIXPTR "zX" +# define PRIi64 PRI64_PREFIX "i" +# define PRIu64 PRI64_PREFIX "u" +# define PRIx64 PRI64_PREFIX "x" +# define PRIX64 PRI64_PREFIX "X" + +/* 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 memchr __builtin_memchr +# 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. */ +void *efi_memchr(const void *p, int c, size_t n); +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/efi.h b/src/boot/efi/efi.h new file mode 100644 index 0000000..fbc5d10 --- /dev/null +++ b/src/boot/efi/efi.h @@ -0,0 +1,459 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#include "macro-fundamental.h" + +#if SD_BOOT +/* uchar.h/wchar.h are not suitable for freestanding environments. */ +typedef __WCHAR_TYPE__ wchar_t; +typedef __CHAR16_TYPE__ char16_t; +typedef __CHAR32_TYPE__ char32_t; + +/* Let's be paranoid and do some sanity checks. */ +assert_cc(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); +assert_cc(__STDC_HOSTED__ == 0); +assert_cc(sizeof(bool) == 1); +assert_cc(sizeof(uint8_t) == 1); +assert_cc(sizeof(uint16_t) == 2); +assert_cc(sizeof(uint32_t) == 4); +assert_cc(sizeof(uint64_t) == 8); +assert_cc(sizeof(wchar_t) == 2); +assert_cc(sizeof(char16_t) == 2); +assert_cc(sizeof(char32_t) == 4); +assert_cc(sizeof(size_t) == sizeof(void *)); +assert_cc(sizeof(size_t) == sizeof(uintptr_t)); +assert_cc(alignof(bool) == 1); +assert_cc(alignof(uint8_t) == 1); +assert_cc(alignof(uint16_t) == 2); +assert_cc(alignof(uint32_t) == 4); +assert_cc(alignof(uint64_t) == 8); +assert_cc(alignof(wchar_t) == 2); +assert_cc(alignof(char16_t) == 2); +assert_cc(alignof(char32_t) == 4); + +# if defined(__x86_64__) && defined(__ILP32__) +# error Building for x64 requires -m64 on x32 ABI. +# endif +#else +# include <uchar.h> +# include <wchar.h> +#endif + +/* We use size_t/ssize_t to represent UEFI UINTN/INTN. */ +typedef size_t EFI_STATUS; +typedef intptr_t ssize_t; + +typedef void* EFI_HANDLE; +typedef void* EFI_EVENT; +typedef size_t EFI_TPL; +typedef uint64_t EFI_LBA; +typedef uint64_t EFI_PHYSICAL_ADDRESS; + +#if defined(__x86_64__) && !defined(__ILP32__) +# define EFIAPI __attribute__((ms_abi)) +#else +# define EFIAPI +#endif + +#if __SIZEOF_POINTER__ == 8 +# define EFI_ERROR_MASK 0x8000000000000000ULL +#elif __SIZEOF_POINTER__ == 4 +# define EFI_ERROR_MASK 0x80000000ULL +#else +# error Unsupported pointer size +#endif + +#define EFIWARN(s) ((EFI_STATUS) s) +#define EFIERR(s) ((EFI_STATUS) (s | EFI_ERROR_MASK)) + +#define EFI_SUCCESS EFIWARN(0) +#define EFI_WARN_UNKNOWN_GLYPH EFIWARN(1) +#define EFI_WARN_DELETE_FAILURE EFIWARN(2) +#define EFI_WARN_WRITE_FAILURE EFIWARN(3) +#define EFI_WARN_BUFFER_TOO_SMALL EFIWARN(4) +#define EFI_WARN_STALE_DATA EFIWARN(5) +#define EFI_WARN_FILE_SYSTEM EFIWARN(6) +#define EFI_WARN_RESET_REQUIRED EFIWARN(7) + +#define EFI_LOAD_ERROR EFIERR(1) +#define EFI_INVALID_PARAMETER EFIERR(2) +#define EFI_UNSUPPORTED EFIERR(3) +#define EFI_BAD_BUFFER_SIZE EFIERR(4) +#define EFI_BUFFER_TOO_SMALL EFIERR(5) +#define EFI_NOT_READY EFIERR(6) +#define EFI_DEVICE_ERROR EFIERR(7) +#define EFI_WRITE_PROTECTED EFIERR(8) +#define EFI_OUT_OF_RESOURCES EFIERR(9) +#define EFI_VOLUME_CORRUPTED EFIERR(10) +#define EFI_VOLUME_FULL EFIERR(11) +#define EFI_NO_MEDIA EFIERR(12) +#define EFI_MEDIA_CHANGED EFIERR(13) +#define EFI_NOT_FOUND EFIERR(14) +#define EFI_ACCESS_DENIED EFIERR(15) +#define EFI_NO_RESPONSE EFIERR(16) +#define EFI_NO_MAPPING EFIERR(17) +#define EFI_TIMEOUT EFIERR(18) +#define EFI_NOT_STARTED EFIERR(19) +#define EFI_ALREADY_STARTED EFIERR(20) +#define EFI_ABORTED EFIERR(21) +#define EFI_ICMP_ERROR EFIERR(22) +#define EFI_TFTP_ERROR EFIERR(23) +#define EFI_PROTOCOL_ERROR EFIERR(24) +#define EFI_INCOMPATIBLE_VERSION EFIERR(25) +#define EFI_SECURITY_VIOLATION EFIERR(26) +#define EFI_CRC_ERROR EFIERR(27) +#define EFI_END_OF_MEDIA EFIERR(28) +#define EFI_ERROR_RESERVED_29 EFIERR(29) +#define EFI_ERROR_RESERVED_30 EFIERR(30) +#define EFI_END_OF_FILE EFIERR(31) +#define EFI_INVALID_LANGUAGE EFIERR(32) +#define EFI_COMPROMISED_DATA EFIERR(33) +#define EFI_IP_ADDRESS_CONFLICT EFIERR(34) +#define EFI_HTTP_ERROR EFIERR(35) + +typedef struct { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; +} EFI_GUID; + +#define GUID_DEF(d1, d2, d3, d4_1, d4_2, d4_3, d4_4, d4_5, d4_6, d4_7, d4_8) \ + { d1, d2, d3, { d4_1, d4_2, d4_3, d4_4, d4_5, d4_6, d4_7, d4_8 } } + +/* Creates a EFI_GUID pointer suitable for EFI APIs. Use of const allows the compiler to merge multiple + * uses (although, currently compilers do that regardless). Most EFI APIs declare their EFI_GUID input + * as non-const, but almost all of them are in fact const. */ +#define MAKE_GUID_PTR(name) ((EFI_GUID *) &(const EFI_GUID) name##_GUID) + +/* These allow MAKE_GUID_PTR() to work without requiring an extra _GUID in the passed name. We want to + * keep the GUID definitions in line with the UEFI spec. */ +#define EFI_GLOBAL_VARIABLE_GUID EFI_GLOBAL_VARIABLE +#define EFI_FILE_INFO_GUID EFI_FILE_INFO_ID + +#define EFI_GLOBAL_VARIABLE \ + GUID_DEF(0x8be4df61, 0x93ca, 0x11d2, 0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c) +#define EFI_IMAGE_SECURITY_DATABASE_GUID \ + GUID_DEF(0xd719b2cb, 0x3d3a, 0x4596, 0xa3, 0xbc, 0xda, 0xd0, 0x0e, 0x67, 0x65, 0x6f) + +#define EVT_TIMER 0x80000000U +#define EVT_RUNTIME 0x40000000U +#define EVT_NOTIFY_WAIT 0x00000100U +#define EVT_NOTIFY_SIGNAL 0x00000200U +#define EVT_SIGNAL_EXIT_BOOT_SERVICES 0x00000201U +#define EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE 0x60000202U + +#define EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL 0x01U +#define EFI_OPEN_PROTOCOL_GET_PROTOCOL 0x02U +#define EFI_OPEN_PROTOCOL_TEST_PROTOCOL 0x04U +#define EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER 0x08U +#define EFI_OPEN_PROTOCOL_BY_DRIVER 0x10U +#define EFI_OPEN_PROTOCOL_EXCLUSIVE 0x20U + +#define EFI_VARIABLE_NON_VOLATILE 0x01U +#define EFI_VARIABLE_BOOTSERVICE_ACCESS 0x02U +#define EFI_VARIABLE_RUNTIME_ACCESS 0x04U +#define EFI_VARIABLE_HARDWARE_ERROR_RECORD 0x08U +#define EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS 0x10U +#define EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS 0x20U +#define EFI_VARIABLE_APPEND_WRITE 0x40U +#define EFI_VARIABLE_ENHANCED_AUTHENTICATED_ACCESS 0x80U + +#define EFI_TIME_ADJUST_DAYLIGHT 0x001U +#define EFI_TIME_IN_DAYLIGHT 0x002U +#define EFI_UNSPECIFIED_TIMEZONE 0x7FFU + +#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x01U +#define EFI_OS_INDICATIONS_TIMESTAMP_REVOCATION 0x02U +#define EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED 0x04U +#define EFI_OS_INDICATIONS_FMP_CAPSULE_SUPPORTED 0x08U +#define EFI_OS_INDICATIONS_CAPSULE_RESULT_VAR_SUPPORTED 0x10U +#define EFI_OS_INDICATIONS_START_OS_RECOVERY 0x20U +#define EFI_OS_INDICATIONS_START_PLATFORM_RECOVERY 0x40U +#define EFI_OS_INDICATIONS_JSON_CONFIG_DATA_REFRESH 0x80U + +#define EFI_PAGE_SIZE 4096U +#define EFI_SIZE_TO_PAGES(s) (((s) + 0xFFFU) >> 12U) + +/* These are common enough to warrant forward declaration. We also give them a + * shorter name for convenience. */ +typedef struct EFI_FILE_PROTOCOL EFI_FILE; +typedef struct EFI_DEVICE_PATH_PROTOCOL EFI_DEVICE_PATH; + +typedef struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL EFI_SIMPLE_TEXT_INPUT_PROTOCOL; +typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; + +typedef enum { + TimerCancel, + TimerPeriodic, + TimerRelative, +} EFI_TIMER_DELAY; + +typedef enum { + AllocateAnyPages, + AllocateMaxAddress, + AllocateAddress, + MaxAllocateType, +} EFI_ALLOCATE_TYPE; + +typedef enum { + EfiReservedMemoryType, + EfiLoaderCode, + EfiLoaderData, + EfiBootServicesCode, + EfiBootServicesData, + EfiRuntimeServicesCode, + EfiRuntimeServicesData, + EfiConventionalMemory, + EfiUnusableMemory, + EfiACPIReclaimMemory, + EfiACPIMemoryNVS, + EfiMemoryMappedIO, + EfiMemoryMappedIOPortSpace, + EfiPalCode, + EfiPersistentMemory, + EfiUnacceptedMemoryType, + EfiMaxMemoryType, +} EFI_MEMORY_TYPE; + +typedef enum { + AllHandles, + ByRegisterNotify, + ByProtocol, +} EFI_LOCATE_SEARCH_TYPE; + +typedef enum { + EfiResetCold, + EfiResetWarm, + EfiResetShutdown, + EfiResetPlatformSpecific, +} EFI_RESET_TYPE; + +typedef struct { + uint16_t Year; + uint8_t Month; + uint8_t Day; + uint8_t Hour; + uint8_t Minute; + uint8_t Second; + uint8_t Pad1; + uint32_t Nanosecond; + int16_t TimeZone; + uint8_t Daylight; + uint8_t Pad2; +} EFI_TIME; + +typedef struct { + uint32_t Resolution; + uint32_t Accuracy; + bool SetsToZero; +} EFI_TIME_CAPABILITIES; + +typedef struct { + uint64_t Signature; + uint32_t Revision; + uint32_t HeaderSize; + uint32_t CRC32; + uint32_t Reserved; +} EFI_TABLE_HEADER; + +typedef struct { + EFI_TABLE_HEADER Hdr; + void *RaiseTPL; + void *RestoreTPL; + EFI_STATUS (EFIAPI *AllocatePages)( + EFI_ALLOCATE_TYPE Type, + EFI_MEMORY_TYPE MemoryType, + size_t Pages, + EFI_PHYSICAL_ADDRESS *Memory); + EFI_STATUS (EFIAPI *FreePages)( + EFI_PHYSICAL_ADDRESS Memory, + size_t Pages); + void *GetMemoryMap; + EFI_STATUS (EFIAPI *AllocatePool)( + EFI_MEMORY_TYPE PoolType, + size_t Size, + void **Buffer); + EFI_STATUS (EFIAPI *FreePool)(void *Buffer); + EFI_STATUS (EFIAPI *CreateEvent)( + uint32_t Type, + EFI_TPL NotifyTpl, + void *NotifyFunction, + void *NotifyContext, + EFI_EVENT *Event); + EFI_STATUS (EFIAPI *SetTimer)( + EFI_EVENT Event, + EFI_TIMER_DELAY Type, + uint64_t TriggerTime); + EFI_STATUS (EFIAPI *WaitForEvent)( + size_t NumberOfEvents, + EFI_EVENT *Event, + size_t *Index); + void *SignalEvent; + EFI_STATUS (EFIAPI *CloseEvent)(EFI_EVENT Event); + EFI_STATUS (EFIAPI *CheckEvent)(EFI_EVENT Event); + void *InstallProtocolInterface; + EFI_STATUS (EFIAPI *ReinstallProtocolInterface)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void *OldInterface, + void *NewInterface); + void *UninstallProtocolInterface; + EFI_STATUS (EFIAPI *HandleProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void **Interface); + void *Reserved; + void *RegisterProtocolNotify; + EFI_STATUS (EFIAPI *LocateHandle)( + EFI_LOCATE_SEARCH_TYPE SearchType, + EFI_GUID *Protocol, + void *SearchKey, + size_t *BufferSize, + EFI_HANDLE *Buffer); + EFI_STATUS (EFIAPI *LocateDevicePath)( + EFI_GUID *Protocol, + EFI_DEVICE_PATH **DevicePath, + EFI_HANDLE *Device); + EFI_STATUS (EFIAPI *InstallConfigurationTable)( + EFI_GUID *Guid, + void *Table); + EFI_STATUS (EFIAPI *LoadImage)( + bool BootPolicy, + EFI_HANDLE ParentImageHandle, + EFI_DEVICE_PATH *DevicePath, + void *SourceBuffer, + size_t SourceSize, + EFI_HANDLE *ImageHandle); + EFI_STATUS (EFIAPI *StartImage)( + EFI_HANDLE ImageHandle, + size_t *ExitDataSize, + char16_t **ExitData); + EFI_STATUS (EFIAPI *Exit)( + EFI_HANDLE ImageHandle, + EFI_STATUS ExitStatus, + size_t ExitDataSize, + char16_t *ExitData); + EFI_STATUS (EFIAPI *UnloadImage)(EFI_HANDLE ImageHandle); + void *ExitBootServices; + EFI_STATUS (EFIAPI *GetNextMonotonicCount)(uint64_t *Count); + EFI_STATUS (EFIAPI *Stall)(size_t Microseconds); + EFI_STATUS (EFIAPI *SetWatchdogTimer)( + size_t Timeout, + uint64_t WatchdogCode, + size_t DataSize, + char16_t *WatchdogData); + EFI_STATUS (EFIAPI *ConnectController)( + EFI_HANDLE ControllerHandle, + EFI_HANDLE *DriverImageHandle, + EFI_DEVICE_PATH *RemainingDevicePath, + bool Recursive); + EFI_STATUS (EFIAPI *DisconnectController)( + EFI_HANDLE ControllerHandle, + EFI_HANDLE DriverImageHandle, + EFI_HANDLE ChildHandle); + EFI_STATUS (EFIAPI *OpenProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void **Interface, + EFI_HANDLE AgentHandle, + EFI_HANDLE ControllerHandle, + uint32_t Attributes); + EFI_STATUS (EFIAPI *CloseProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + EFI_HANDLE AgentHandle, + EFI_HANDLE ControllerHandle); + void *OpenProtocolInformation; + EFI_STATUS (EFIAPI *ProtocolsPerHandle)( + EFI_HANDLE Handle, + EFI_GUID ***ProtocolBuffer, + size_t *ProtocolBufferCount); + EFI_STATUS (EFIAPI *LocateHandleBuffer)( + EFI_LOCATE_SEARCH_TYPE SearchType, + EFI_GUID *Protocol, + void *SearchKey, + size_t *NoHandles, + EFI_HANDLE **Buffer); + EFI_STATUS (EFIAPI *LocateProtocol)( + EFI_GUID *Protocol, + void *Registration, + void **Interface); + EFI_STATUS (EFIAPI *InstallMultipleProtocolInterfaces)(EFI_HANDLE *Handle, ...); + EFI_STATUS (EFIAPI *UninstallMultipleProtocolInterfaces)(EFI_HANDLE Handle, ...); + EFI_STATUS (EFIAPI *CalculateCrc32)( + void *Data, + size_t DataSize, + uint32_t *Crc32); + void (EFIAPI *CopyMem)( + void *Destination, + void *Source, + size_t Length); + void (EFIAPI *SetMem)( + void *Buffer, + size_t Size, + uint8_t Value); + void *CreateEventEx; +} EFI_BOOT_SERVICES; + +typedef struct { + EFI_TABLE_HEADER Hdr; + EFI_STATUS (EFIAPI *GetTime)( + EFI_TIME *Time, + EFI_TIME_CAPABILITIES *Capabilities); + EFI_STATUS (EFIAPI *SetTime)(EFI_TIME *Time); + void *GetWakeupTime; + void *SetWakeupTime; + void *SetVirtualAddressMap; + void *ConvertPointer; + EFI_STATUS (EFIAPI *GetVariable)( + char16_t *VariableName, + EFI_GUID *VendorGuid, + uint32_t *Attributes, + size_t *DataSize, + void *Data); + void *GetNextVariableName; + EFI_STATUS (EFIAPI *SetVariable)( + char16_t *VariableName, + EFI_GUID *VendorGuid, + uint32_t Attributes, + size_t DataSize, + void *Data); + EFI_STATUS (EFIAPI *GetNextHighMonotonicCount)(uint32_t *HighCount); + void (EFIAPI *ResetSystem)( + EFI_RESET_TYPE ResetType, + EFI_STATUS ResetStatus, + size_t DataSize, + void *ResetData); + void *UpdateCapsule; + void *QueryCapsuleCapabilities; + void *QueryVariableInfo; +} EFI_RUNTIME_SERVICES; + +typedef struct { + EFI_TABLE_HEADER Hdr; + char16_t *FirmwareVendor; + uint32_t FirmwareRevision; + EFI_HANDLE ConsoleInHandle; + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; + EFI_HANDLE ConsoleOutHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; + EFI_HANDLE StandardErrorHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; + EFI_RUNTIME_SERVICES *RuntimeServices; + EFI_BOOT_SERVICES *BootServices; + size_t NumberOfTableEntries; + struct { + EFI_GUID VendorGuid; + void *VendorTable; + } *ConfigurationTable; +} EFI_SYSTEM_TABLE; + +extern EFI_SYSTEM_TABLE *ST; +extern EFI_BOOT_SERVICES *BS; +extern EFI_RUNTIME_SERVICES *RT; diff --git a/src/boot/efi/fuzz-bcd.c b/src/boot/efi/fuzz-bcd.c new file mode 100644 index 0000000..cb5be7a --- /dev/null +++ b/src/boot/efi/fuzz-bcd.c @@ -0,0 +1,24 @@ +/* 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; + + fuzz_setup_logging(); + + 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-osrel.c b/src/boot/efi/fuzz-efi-osrel.c new file mode 100644 index 0000000..1a5a9bc --- /dev/null +++ b/src/boot/efi/fuzz-efi-osrel.c @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "efi-string.h" +#include "fuzz.h" + +#define SEP_LEN 4 + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (outside_size_range(size, SEP_LEN + 1, 64 * 1024)) + return 0; + if (data[SEP_LEN] != '\0') + return 0; + + fuzz_setup_logging(); + + _cleanup_free_ char *p = memdup_suffix0(data + SEP_LEN + 1, size - SEP_LEN - 1); + assert_se(p); + + size_t pos = 0; + char *key, *value; + while (line_get_key_value(p, (const char *) data, &pos, &key, &value)) { + assert_se(key); + assert_se(value); + } + + return 0; +} diff --git a/src/boot/efi/fuzz-efi-printf.c b/src/boot/efi/fuzz-efi-printf.c new file mode 100644 index 0000000..6dee830 --- /dev/null +++ b/src/boot/efi/fuzz-efi-printf.c @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "efi-string.h" +#include "fuzz.h" +#include "utf8.h" + +typedef struct { + EFI_STATUS status; + int16_t field_width; + int16_t precision; + const void *ptr; + char c; + unsigned char uchar; + signed char schar; + unsigned short ushort; + signed short sshort; + unsigned int uint; + signed int sint; + unsigned long ulong; + signed long slong; + unsigned long long ulonglong; + signed long long slonglong; + size_t size; + ssize_t ssize; + intmax_t intmax; + uintmax_t uintmax; + ptrdiff_t ptrdiff; + char str[]; +} Input; + +#define PRINTF_ONE(...) \ + ({ \ + _cleanup_free_ char16_t *_ret = xasprintf_status(__VA_ARGS__); \ + DO_NOT_OPTIMIZE(_ret); \ + }) + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (outside_size_range(size, sizeof(Input), 1024 * 1024)) + return 0; + + const Input *i = (const Input *) data; + size_t len = size - offsetof(Input, str); + + fuzz_setup_logging(); + + PRINTF_ONE(i->status, "%*.*s", i->field_width, (int) len, i->str); + PRINTF_ONE(i->status, "%*.*ls", i->field_width, (int) (len / sizeof(wchar_t)), (const wchar_t *) i->str); + + PRINTF_ONE(i->status, "%% %*.*m", i->field_width, i->precision); + PRINTF_ONE(i->status, "%*p", i->field_width, i->ptr); + PRINTF_ONE(i->status, "%*c %12340c %56789c", i->field_width, i->c, i->c, i->c); + + PRINTF_ONE(i->status, "%*.*hhu", i->field_width, i->precision, i->uchar); + PRINTF_ONE(i->status, "%*.*hhi", i->field_width, i->precision, i->schar); + PRINTF_ONE(i->status, "%*.*hu", i->field_width, i->precision, i->ushort); + PRINTF_ONE(i->status, "%*.*hi", i->field_width, i->precision, i->sshort); + PRINTF_ONE(i->status, "%*.*u", i->field_width, i->precision, i->uint); + PRINTF_ONE(i->status, "%*.*i", i->field_width, i->precision, i->sint); + PRINTF_ONE(i->status, "%*.*lu", i->field_width, i->precision, i->ulong); + PRINTF_ONE(i->status, "%*.*li", i->field_width, i->precision, i->slong); + PRINTF_ONE(i->status, "%*.*llu", i->field_width, i->precision, i->ulonglong); + PRINTF_ONE(i->status, "%*.*lli", i->field_width, i->precision, i->slonglong); + + PRINTF_ONE(i->status, "%+*.*hhi", i->field_width, i->precision, i->schar); + PRINTF_ONE(i->status, "%-*.*hi", i->field_width, i->precision, i->sshort); + PRINTF_ONE(i->status, "% *.*i", i->field_width, i->precision, i->sint); + PRINTF_ONE(i->status, "%0*li", i->field_width, i->slong); + PRINTF_ONE(i->status, "%#*.*llx", i->field_width, i->precision, i->ulonglong); + + PRINTF_ONE(i->status, "%-*.*zx", i->field_width, i->precision, i->size); + PRINTF_ONE(i->status, "% *.*zi", i->field_width, i->precision, i->ssize); + PRINTF_ONE(i->status, "%0*ji", i->field_width, i->intmax); + PRINTF_ONE(i->status, "%#0*jX", i->field_width, i->uintmax); + PRINTF_ONE(i->status, "%*.*ti", i->field_width, i->precision, i->ptrdiff); + + 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..36ecaf9 --- /dev/null +++ b/src/boot/efi/fuzz-efi-string.c @@ -0,0 +1,42 @@ +/* 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; + + fuzz_setup_logging(); + + 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..496fc69 --- /dev/null +++ b/src/boot/efi/graphics.c @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright © 2013 Intel Corporation + * Authored by Joonas Lahtinen <joonas.lahtinen@linux.intel.com> + */ + +#include "graphics.h" +#include "proto/console-control.h" +#include "proto/simple-text-io.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; + bool uga_exists, stdin_locked; + EFI_STATUS err; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_CONSOLE_CONTROL_PROTOCOL), 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; + + log_wait(); + 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..33ab7f8 --- /dev/null +++ b/src/boot/efi/graphics.h @@ -0,0 +1,10 @@ +/* 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" + +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..527b05f --- /dev/null +++ b/src/boot/efi/initrd.c @@ -0,0 +1,136 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "initrd.h" +#include "macro-fundamental.h" +#include "proto/device-path.h" +#include "proto/load-file.h" +#include "util.h" + +#define LINUX_INITRD_MEDIA_GUID \ + GUID_DEF(0x5568e427, 0x68fc, 0x4f3d, 0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68) + +/* extend LoadFileProtocol */ +struct initrd_loader { + EFI_LOAD_FILE_PROTOCOL load_file; + const void *address; + size_t 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), + }, + .Guid = LINUX_INITRD_MEDIA_GUID + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = sizeof(efi_initrd_device_path.end), + } +}; + +static EFIAPI EFI_STATUS initrd_load_file( + EFI_LOAD_FILE_PROTOCOL *this, + EFI_DEVICE_PATH *file_path, + bool boot_policy, + size_t *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, + size_t 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(MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), &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, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), + &efi_initrd_device_path, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), + 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->HandleProtocol(initrd_handle, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), (void **) &loader); + if (err != EFI_SUCCESS) + return err; + + /* uninstall all protocols thus destroying the handle */ + err = BS->UninstallMultipleProtocolInterfaces( + initrd_handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), + &efi_initrd_device_path, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), + 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..e7685ae --- /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, + size_t 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..65bc176 --- /dev/null +++ b/src/boot/efi/linux.c @@ -0,0 +1,156 @@ +/* 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 "initrd.h" +#include "linux.h" +#include "pe.h" +#include "proto/device-path.h" +#include "proto/loaded-image.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), + }, + .Guid = STUB_PAYLOAD_GUID, + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = sizeof(payload_device_path.end), + }, + }; + + /* 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(err, "Bad kernel image: %m"); + + _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(err, "Error loading kernel image: %m"); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol( + kernel_image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting kernel loaded image protocol: %m"); + + 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(err, "Error registering initrd: %m"); + + log_wait(); + 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(err, "Error starting kernel image: %m"); +} diff --git a/src/boot/efi/linux.h b/src/boot/efi/linux.h new file mode 100644 index 0000000..46b5f4f --- /dev/null +++ b/src/boot/efi/linux.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.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..757902d --- /dev/null +++ b/src/boot/efi/linux_x86.c @@ -0,0 +1,224 @@ +/* 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 "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; /* 32-bit entry address. */ + + /* Old kernels needs this set, while newer ones seem to ignore this. */ + params->hdr.code32_start = kernel; + +#ifdef __x86_64__ + kernel += KERNEL_SECTOR_SIZE; /* 64-bit entry address. */ +#endif + + kernel += params->hdr.handover_offset; /* 32/64-bit EFI handover address. */ + + /* Note in EFI mixed mode this now points to the correct 32-bit handover entry point, allowing a 64-bit + * kernel to be booted from a 32-bit 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(EFI_UNSUPPORTED, "Unsupported kernel image."); + if (image_params->hdr.version < SETUP_VERSION_2_11) + return log_error_status(EFI_UNSUPPORTED, "Kernel too old."); + if (!image_params->hdr.relocatable_kernel) + return log_error_status(EFI_UNSUPPORTED, "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(EFI_UNSUPPORTED, "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); + + /* There is no way to pass the high bits of code32_start. Newer kernels seems to handle this + * just fine, but older kernels will fail even if they otherwise have above 4G boot support. */ + _cleanup_pages_ Pages linux_relocated = {}; + if (POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + linux_length > UINT32_MAX) { + linux_relocated = xmalloc_pages( + AllocateMaxAddress, EfiLoaderCode, EFI_SIZE_TO_PAGES(linux_length), UINT32_MAX); + linux_buffer = memcpy( + PHYSICAL_ADDRESS_TO_POINTER(linux_relocated.addr), linux_buffer, linux_length); + } + + _cleanup_pages_ Pages initrd_relocated = {}; + if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) + initrd_length > UINT32_MAX) { + initrd_relocated = xmalloc_pages( + AllocateMaxAddress, EfiLoaderData, EFI_SIZE_TO_PAGES(initrd_length), UINT32_MAX); + initrd_buffer = memcpy( + PHYSICAL_ADDRESS_TO_POINTER(initrd_relocated.addr), + initrd_buffer, + initrd_length); + } + + _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; + + log_wait(); + linux_efi_handover(parent, (uintptr_t) linux_buffer, boot_params); + return EFI_LOAD_ERROR; +} diff --git a/src/boot/efi/log.c b/src/boot/efi/log.c new file mode 100644 index 0000000..364471e --- /dev/null +++ b/src/boot/efi/log.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "log.h" +#include "proto/rng.h" +#include "proto/simple-text-io.h" +#include "util.h" + +static unsigned log_count = 0; + +void freeze(void) { + for (;;) + BS->Stall(60 * 1000 * 1000); +} + +_noreturn_ static void panic(const char16_t *message) { + if (ST->ConOut->Mode->CursorColumn > 0) + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, EFI_TEXT_ATTR(EFI_LIGHTRED, EFI_BLACK)); + ST->ConOut->OutputString(ST->ConOut, (char16_t *) message); + freeze(); +} + +void efi_assert(const char *expr, const char *file, unsigned line, const char *function) { + static bool asserting = false; + + /* Let's be paranoid. */ + if (asserting) + panic(u"systemd-boot: Nested assertion failure, halting."); + + asserting = true; + log_error("systemd-boot: Assertion '%s' failed at %s:%u@%s, halting.", expr, file, line, function); + freeze(); +} + +EFI_STATUS log_internal(EFI_STATUS status, const char *format, ...) { + assert(format); + + int32_t attr = ST->ConOut->Mode->Attribute; + + if (ST->ConOut->Mode->CursorColumn > 0) + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, EFI_TEXT_ATTR(EFI_LIGHTRED, EFI_BLACK)); + + va_list ap; + va_start(ap, format); + vprintf_status(status, format, ap); + va_end(ap); + + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, attr); + + log_count++; + return status; +} + +#ifdef EFI_DEBUG +void log_hexdump(const char16_t *prefix, const void *data, size_t size) { + /* Debugging helper — please keep this around, even if not used */ + + _cleanup_free_ char16_t *hex = hexdump(data, size); + log_internal(EFI_SUCCESS, "%ls[%zu]: %ls", prefix, size, hex); +} +#endif + +void log_wait(void) { + if (log_count == 0) + return; + + BS->Stall(MIN(4u, log_count) * 2500 * 1000); + log_count = 0; +} + +_used_ intptr_t __stack_chk_guard = (intptr_t) 0x70f6967de78acae3; + +/* We can only set a random stack canary if this function attribute is available, + * otherwise this may create a stack check fail. */ +#if STACK_PROTECTOR_RANDOM +void __stack_chk_guard_init(void) { + EFI_RNG_PROTOCOL *rng; + if (BS->LocateProtocol(MAKE_GUID_PTR(EFI_RNG_PROTOCOL), NULL, (void **) &rng) == EFI_SUCCESS) + (void) rng->GetRNG(rng, NULL, sizeof(__stack_chk_guard), (void *) &__stack_chk_guard); + else + /* Better than no extra entropy. */ + __stack_chk_guard ^= (intptr_t) __executable_start; +} +#endif + +_used_ _noreturn_ void __stack_chk_fail(void); +_used_ _noreturn_ void __stack_chk_fail_local(void); +void __stack_chk_fail(void) { + panic(u"systemd-boot: Stack check failed, halting."); +} +void __stack_chk_fail_local(void) { + __stack_chk_fail(); +} + +/* Called by libgcc for some fatal errors like integer overflow with -ftrapv. */ +_used_ _noreturn_ void abort(void); +void abort(void) { + panic(u"systemd-boot: Unknown error, halting."); +} + +#if defined(__ARM_EABI__) +/* These override the (weak) div0 handlers from libgcc as they would otherwise call raise() instead. */ +_used_ _noreturn_ int __aeabi_idiv0(int return_value); +_used_ _noreturn_ long long __aeabi_ldiv0(long long return_value); + +int __aeabi_idiv0(int return_value) { + panic(u"systemd-boot: Division by zero, halting."); +} + +long long __aeabi_ldiv0(long long return_value) { + panic(u"systemd-boot: Division by zero, halting."); +} +#endif diff --git a/src/boot/efi/log.h b/src/boot/efi/log.h new file mode 100644 index 0000000..13f3887 --- /dev/null +++ b/src/boot/efi/log.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi-string.h" + +#if defined __has_attribute +# if __has_attribute(no_stack_protector) +# define HAVE_NO_STACK_PROTECTOR_ATTRIBUTE +# endif +#endif + +#if defined(HAVE_NO_STACK_PROTECTOR_ATTRIBUTE) && \ + (defined(__SSP__) || defined(__SSP_ALL__) || \ + defined(__SSP_STRONG__) || defined(__SSP_EXPLICIT__)) +# define STACK_PROTECTOR_RANDOM 1 +__attribute__((no_stack_protector, noinline)) void __stack_chk_guard_init(void); +#else +# define STACK_PROTECTOR_RANDOM 0 +# define __stack_chk_guard_init() +#endif + +_noreturn_ void freeze(void); +void log_wait(void); +_gnu_printf_(2, 3) EFI_STATUS log_internal(EFI_STATUS status, const char *format, ...); +#define log_error_status(status, ...) log_internal(status, __VA_ARGS__) +#define log_error(...) log_internal(EFI_INVALID_PARAMETER, __VA_ARGS__) +#define log_oom() log_internal(EFI_OUT_OF_RESOURCES, "Out of memory.") +#define log_trace() log_internal(EFI_SUCCESS, "%s:%i@%s", __FILE__, __LINE__, __func__) + +#ifdef EFI_DEBUG +void log_hexdump(const char16_t *prefix, const void *data, size_t size); +#endif diff --git a/src/boot/efi/measure.c b/src/boot/efi/measure.c new file mode 100644 index 0000000..01c97c8 --- /dev/null +++ b/src/boot/efi/measure.c @@ -0,0 +1,296 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if ENABLE_TPM + +#include "macro-fundamental.h" +#include "measure.h" +#include "memory-util-fundamental.h" +#include "proto/tcg.h" +#include "tpm2-pcr.h" +#include "util.h" + +static EFI_STATUS tpm1_measure_to_pcr_and_event_log( + const EFI_TCG_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + size_t 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; + size_t desc_len; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + tcg_event = xmalloc(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_PROTOCOL *) tcg, + buffer, buffer_size, + TCG_ALG_SHA, + tcg_event, + &event_number, + &event_log_last); +} + +static EFI_STATUS tpm2_measure_to_pcr_and_tagged_event_log( + EFI_TCG2_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + uint64_t buffer_size, + uint32_t event_id, + const char16_t *description) { + + _cleanup_free_ struct event { + EFI_TCG2_EVENT tcg_event; + EFI_TCG2_TAGGED_EVENT tcg_tagged_event; + } _packed_ *event = NULL; + size_t desc_len, event_size; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + event_size = offsetof(EFI_TCG2_EVENT, Event) + offsetof(EFI_TCG2_TAGGED_EVENT, Event) + desc_len; + + event = xmalloc(event_size); + *event = (struct event) { + .tcg_event = (EFI_TCG2_EVENT) { + .Size = event_size, + .Header.HeaderSize = sizeof(EFI_TCG2_EVENT_HEADER), + .Header.HeaderVersion = EFI_TCG2_EVENT_HEADER_VERSION, + .Header.PCRIndex = pcrindex, + .Header.EventType = EV_EVENT_TAG, + }, + .tcg_tagged_event = { + .EventId = event_id, + .EventSize = desc_len, + }, + }; + memcpy(event->tcg_tagged_event.Event, description, desc_len); + + return tcg->HashLogExtendEvent( + tcg, + 0, + buffer, buffer_size, + &event->tcg_event); +} + +static EFI_STATUS tpm2_measure_to_pcr_and_event_log( + EFI_TCG2_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + uint64_t buffer_size, + const char16_t *description) { + + _cleanup_free_ EFI_TCG2_EVENT *tcg_event = NULL; + size_t desc_len; + + assert(tcg); + assert(description); + + /* NB: We currently record everything as EV_IPL. Which sucks, because it makes it hard to + * recognize from the event log which of the events are ours. Measurement logs are kinda API hence + * this is hard to change for existing, established events. But for future additions, let's use + * EV_EVENT_TAG instead, with a tag of our choosing that makes clear what precisely we are measuring + * here. */ + + desc_len = strsize16(description); + tcg_event = xmalloc(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_PROTOCOL *tcg1_interface_check(void) { + EFI_PHYSICAL_ADDRESS event_log_location, event_log_last_entry; + EFI_TCG_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + uint32_t features; + EFI_TCG_PROTOCOL *tcg; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_TCG_PROTOCOL), 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_PROTOCOL *tcg2_interface_check(void) { + EFI_TCG2_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + EFI_TCG2_PROTOCOL *tcg; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_TCG2_PROTOCOL), 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) { + EFI_TCG_BOOT_SERVICE_CAPABILITY *caps_1_0 = + (EFI_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, size_t buffer_size, const char16_t *description, bool *ret_measured) { + EFI_TCG2_PROTOCOL *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_PROTOCOL *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_tagged_event( + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + size_t buffer_size, + uint32_t event_id, + const char16_t *description, + bool *ret_measured) { + + EFI_TCG2_PROTOCOL *tpm2; + EFI_STATUS err; + + assert(description || pcrindex == UINT32_MAX); + assert(event_id > 0); + + /* If EFI_SUCCESS is returned, will initialize ret_measured to true if we actually measured + * something, or false if measurement was turned off. */ + + tpm2 = tcg2_interface_check(); + if (!tpm2 || pcrindex == UINT32_MAX) { /* PCR disabled? */ + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; + } + + err = tpm2_measure_to_pcr_and_tagged_event_log(tpm2, pcrindex, buffer, buffer_size, event_id, description); + if (err == EFI_SUCCESS && ret_measured) + *ret_measured = true; + + return err; +} + +EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t 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) { + bool measured = false; + EFI_STATUS err; + + /* Measures a load options string into the TPM2, i.e. the kernel command line */ + + err = tpm_log_event( + TPM2_PCR_KERNEL_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(load_options), + strsize16(load_options), + load_options, + &measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add load options (i.e. kernel command) line measurement to PCR %i: %m", + TPM2_PCR_KERNEL_CONFIG); + + if (ret_measured) + *ret_measured = 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..c3c4e0a --- /dev/null +++ b/src/boot/efi/measure.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#if ENABLE_TPM + +bool tpm_present(void); +EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char16_t *description, bool *ret_measured); +EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char *description, bool *ret_measured); +EFI_STATUS tpm_log_tagged_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, uint32_t event_id, const char16_t *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, size_t 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, size_t buffer_size, const char *description, bool *ret_measured) { + if (ret_measured) + *ret_measured = false; + return EFI_SUCCESS; +} + +static inline EFI_STATUS tpm_log_tagged_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, uint32_t event_id, const char16_t *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..c95132e --- /dev/null +++ b/src/boot/efi/meson.build @@ -0,0 +1,409 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +efi_config_h_dir = meson.current_build_dir() +efi_addon = '' + +libefitest = static_library( + 'efitest', + files( + 'bcd.c', + 'efi-string.c', + ), + build_by_default : false, + include_directories : [ + basic_includes, + include_directories('.'), + ], + dependencies : userspace) + +efitest_base = { + 'link_with' : [ + libefitest, + libshared, + ], +} +efi_test_template = test_template + efitest_base +efi_fuzz_template = fuzz_template + efitest_base + +executables += [ + efi_test_template + { + 'sources' : files('test-bcd.c'), + 'dependencies' : libzstd, + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_ZSTD'], + }, + efi_test_template + { + 'sources' : files('test-efi-string.c'), + 'conditions' : ['ENABLE_BOOTLOADER'], + }, + efi_fuzz_template + { + 'sources' : files('fuzz-bcd.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-string.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-osrel.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-printf.c'), + }, +] + +if conf.get('ENABLE_BOOTLOADER') != 1 + subdir_done() +endif + +efi_conf = configuration_data() +efi_conf.set10('ENABLE_TPM', get_option('tpm')) + +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 == '' + # This is determined during build, not configuration, so we can't display it yet. + efi_conf.set('SBAT_DISTRO_VERSION', 'GIT_VERSION') + else + efi_conf.set_quoted('SBAT_DISTRO_VERSION', pkgver) + endif +endif + +summary({'UEFI architectures' : efi_arch + (efi_arch_alt == '' ? '' : ', ' + efi_arch_alt)}, + section : 'UEFI') + +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': efi_conf.get('SBAT_DISTRO_VERSION'), + 'SBAT distro summary': efi_conf.get('SBAT_DISTRO_SUMMARY'), + 'SBAT distro URL': efi_conf.get('SBAT_DISTRO_URL')}, + section : 'UEFI') +endif + +configure_file( + output : 'efi_config.h', + configuration : efi_conf) + +############################################################ + +efi_includes = [ + build_dir_include, + fundamental_include, + include_directories('.'), +] + +efi_c_args = [ + '-DSD_BOOT=1', + '-ffreestanding', + '-fno-strict-aliasing', + '-fshort-wchar', + '-include', 'efi_config.h', +] + +efi_c_args += cc.get_supported_arguments( + '-fwide-exec-charset=UCS2', + # gcc docs says this is required for ms_abi to work correctly. + '-maccumulate-outgoing-args', + '-mstack-protector-guard=global', +) + +# Debug information has little value in release builds as no normal human being knows +# how to attach a debugger to EFI binaries running on real hardware. Anyone who does +# certainly has the means to do their own dev build. +if get_option('mode') == 'developer' and get_option('debug') + efi_c_args += '-DEFI_DEBUG' +endif + +efi_c_ld_args = [ + '-lgcc', + '-nostdlib', + '-static-pie', + '-Wl,--entry=efi_main', + '-Wl,--fatal-warnings', + + # These flags should be passed by -static-pie, but for whatever reason the flag translation + # is not enabled on all architectures. Not passing `-static` would just allow the linker to + # use dynamic libraries, (which we can't/don't use anyway). But if `-pie` is missing and the + # gcc build does not default to `-pie` we get a regular (no-pie) binary that will be + # rightfully rejected by elf2efi. Note that meson also passes `-pie` to the linker driver, + # but it is overridden by our `-static-pie`. We also need to pass these directly to the + # linker as `-static`+`-pie` seem to get translated differently. + '-Wl,-static,-pie,--no-dynamic-linker,-z,text', + + # EFI has 4KiB pages. + '-z', 'common-page-size=4096', + '-z', 'max-page-size=4096', + + '-z', 'noexecstack', + '-z', 'relro', + '-z', 'separate-code', +] + +efi_c_ld_args += cc.get_supported_link_arguments( + # binutils >= 2.38 + '-Wl,-z,nopack-relative-relocs', +) + +# efi_c_args is explicitly passed to targets so that they can override distro-provided flags +# that should not be used for EFI binaries. +efi_disabled_c_args = cc.get_supported_arguments( + '-fcf-protection=none', + '-fno-asynchronous-unwind-tables', + '-fno-exceptions', + '-fno-unwind-tables', +) +efi_override_options = [ + 'b_coverage=false', + 'b_pgo=off', +] + +if get_option('b_sanitize') == 'undefined' + efi_disabled_c_args += cc.get_supported_arguments('-fno-sanitize-link-runtime') +else + efi_disabled_c_args += cc.get_supported_arguments('-fno-sanitize=all') + efi_override_options += 'b_sanitize=none' +endif + +efi_c_args += efi_disabled_c_args +efi_c_ld_args += efi_disabled_c_args + +if cc.get_id() == 'clang' + # clang is too picky sometimes. + efi_c_args += '-Wno-unused-command-line-argument' + efi_c_ld_args += '-Wno-unused-command-line-argument' +endif + +efi_arch_c_args = { + 'aarch64' : ['-mgeneral-regs-only'], + 'arm' : ['-mgeneral-regs-only'], + # Pass -m64/32 explicitly to make building on x32 work. + 'x86_64' : ['-m64', '-march=x86-64', '-mno-red-zone', '-mgeneral-regs-only'], + 'x86' : ['-m32', '-march=i686', '-mgeneral-regs-only', '-malign-double'], +} +efi_arch_c_ld_args = { + # libgcc is not compiled with -fshort-wchar, but it does not use it anyways, + # so it's fine to link against it. + 'arm' : cc.get_supported_link_arguments('-Wl,--no-wchar-size-warning'), + 'x86_64' : ['-m64'], + 'x86' : ['-m32'], +} + +linker_sanity_code = 'void a(void) {}; void _start(void) { a(); }' +linker_sanity_args = ['-nostdlib', '-Wl,--fatal-warnings'] +if not cc.links(linker_sanity_code, + name : 'linker supports -static-pie', + args : [linker_sanity_args, '-static-pie']) + error('Linker does not support -static-pie.') +endif + +# https://github.com/llvm/llvm-project/issues/67152 +if not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib', + args : [linker_sanity_args, '-flto']) + efi_c_args += '-fno-lto' + efi_c_ld_args += '-fno-lto' +endif + +# https://github.com/llvm/llvm-project/issues/61101 +if efi_cpu_family_alt == 'x86' and not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib (x86)', + args : [linker_sanity_args, '-flto', '-m32']) + efi_arch_c_args += { 'x86' : efi_arch_c_args['x86'] + '-fno-lto' } + efi_arch_c_ld_args += { 'x86' : efi_arch_c_ld_args['x86'] + '-fno-lto' } +endif + +############################################################ + +libefi_sources = files( + 'console.c', + 'device-path-util.c', + 'devicetree.c', + 'drivers.c', + 'efi-string.c', + 'graphics.c', + 'initrd.c', + 'log.c', + 'measure.c', + 'part-discovery.c', + 'pe.c', + 'random-seed.c', + 'secure-boot.c', + 'shim.c', + 'ticks.c', + 'util.c', + 'vmm.c', +) + +systemd_boot_sources = files( + 'boot.c', +) + +stub_sources = files( + 'cpio.c', + 'linux.c', + 'splash.c', + 'stub.c', +) + +addon_sources = files( + 'addon.c', +) + +if get_option('b_sanitize') == 'undefined' + libefi_sources += files('ubsan.c') +endif + +if host_machine.cpu_family() in ['x86', 'x86_64'] + stub_sources += files('linux_x86.c') +endif + +# BCD parser only makes sense on arches that Windows supports. +if host_machine.cpu_family() in ['aarch64', 'arm', 'x86_64', 'x86'] + systemd_boot_sources += files('bcd.c') +endif + +boot_targets = [] +efi_elf_binaries = [] +efi_archspecs = [ + { + 'arch' : efi_arch, + 'c_args' : [ + efi_c_args, + '-DEFI_MACHINE_TYPE_NAME="' + efi_arch + '"', + efi_arch_c_args.get(host_machine.cpu_family(), []), + ], + 'link_args' : [ + efi_c_ld_args, + efi_arch_c_ld_args.get(host_machine.cpu_family(), []), + ], + }, +] +if efi_arch_alt != '' + efi_archspecs += { + 'arch' : efi_arch_alt, + 'c_args' : [ + efi_c_args, + '-DEFI_MACHINE_TYPE_NAME="' + efi_arch_alt + '"', + efi_arch_c_args.get(efi_cpu_family_alt, []), + ], + 'link_args' : [ + efi_c_ld_args, + efi_arch_c_ld_args.get(efi_cpu_family_alt, []), + ], + } +endif + +foreach archspec : efi_archspecs + libefi = static_library( + 'efi' + archspec['arch'], + fundamental_sources, + libefi_sources, + version_h, + include_directories : efi_includes, + c_args : archspec['c_args'], + gnu_symbol_visibility : 'hidden', + override_options : efi_override_options, + pic : true) + + kwargs = { + 'include_directories' : efi_includes, + 'c_args' : archspec['c_args'], + 'link_args' : archspec['link_args'], + 'gnu_symbol_visibility' : 'hidden', + 'override_options' : efi_override_options, + 'pie' : true, + } + + efi_elf_binaries += executable( + 'systemd-boot' + archspec['arch'], + sources : [systemd_boot_sources, version_h], + link_with : libefi, + name_suffix : 'elf', + kwargs : kwargs) + + efi_elf_binaries += executable( + 'linux' + archspec['arch'], + sources : [stub_sources, version_h], + link_with : libefi, + name_suffix : 'elf.stub', + kwargs : kwargs) + + efi_elf_binaries += executable( + 'addon' + archspec['arch'], + sources : [addon_sources, version_h], + name_suffix : 'elf.stub', + kwargs : kwargs) +endforeach + +foreach efi_elf_binary : efi_elf_binaries + name = efi_elf_binary.name() + name += name.startswith('systemd-boot') ? '.efi' : '.efi.stub' + # For the addon, given it's empty, we need to explicitly reserve space in the header to account for + # the sections that ukify will add. + minimum_sections = name.endswith('.stub') ? '15' : '0' + exe = custom_target( + name, + output : name, + input : efi_elf_binary, + install : true, + install_dir : bootlibdir, + install_tag : 'systemd-boot', + command : [ + elf2efi_py, + '--version-major=' + meson.project_version(), + '--version-minor=0', + '--efi-major=1', + '--efi-minor=1', + '--subsystem=10', + '--minimum-sections=' + minimum_sections, + '--copy-sections=.sbat,.sdmagic,.osrel', + '@INPUT@', + '@OUTPUT@', + ]) + boot_targets += exe + if name.startswith('linux') + boot_stubs += exe + endif + + # This is supposed to match exactly one time + if name == 'addon@0@.efi.stub'.format(efi_arch) + efi_addon = exe.full_path() + endif +endforeach + +alias_target('systemd-boot', boot_targets) diff --git a/src/boot/efi/part-discovery.c b/src/boot/efi/part-discovery.c new file mode 100644 index 0000000..f5b1573 --- /dev/null +++ b/src/boot/efi/part-discovery.c @@ -0,0 +1,298 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "part-discovery.h" +#include "proto/block-io.h" +#include "proto/device-path.h" +#include "util.h" + +typedef struct { + EFI_GUID PartitionTypeGUID; + EFI_GUID UniquePartitionGUID; + EFI_LBA StartingLBA; + EFI_LBA EndingLBA; + uint64_t Attributes; + char16_t PartitionName[36]; +} EFI_PARTITION_ENTRY; + +typedef struct { + EFI_TABLE_HEADER Header; + EFI_LBA MyLBA; + EFI_LBA AlternateLBA; + EFI_LBA FirstUsableLBA; + EFI_LBA LastUsableLBA; + EFI_GUID DiskGUID; + EFI_LBA PartitionEntryLBA; + uint32_t NumberOfPartitionEntries; + uint32_t SizeOfPartitionEntry; + uint32_t PartitionEntryArrayCRC32; + uint8_t _pad[420]; +} _packed_ GptHeader; +assert_cc(sizeof(GptHeader) == 512); + +static bool verify_gpt(/*const*/ GptHeader *h, EFI_LBA lba_expected) { + uint32_t crc32, crc32_saved; + EFI_STATUS err; + + assert(h); + + /* 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(h, 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 > SIZE_MAX / h->NumberOfPartitionEntries) + return false; + + return true; +} + +static EFI_STATUS try_gpt( + const EFI_GUID *type, + 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; + GptHeader gpt; + EFI_STATUS err; + uint32_t crc32; + size_t 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.AlternateLBA; + + if (!verify_gpt(&gpt, lba)) + return EFI_NOT_FOUND; + + /* Now load the GPT entry table */ + size = ALIGN_TO((size_t) gpt.SizeOfPartitionEntry * (size_t) gpt.NumberOfPartitionEntries, 512); + entries = xmalloc(size); + + err = block_io->ReadBlocks( + block_io, + block_io->Media->MediaId, + gpt.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.PartitionEntryArrayCRC32) + return EFI_CRC_ERROR; + + /* Now we can finally look for xbootloader partitions. */ + for (size_t i = 0; i < gpt.NumberOfPartitionEntries; i++) { + EFI_PARTITION_ENTRY *entry = + (EFI_PARTITION_ENTRY *) ((uint8_t *) entries + gpt.SizeOfPartitionEntry * i); + + if (!efi_guid_equal(&entry->PartitionTypeGUID, type)) + continue; + + if (entry->EndingLBA < entry->StartingLBA) /* Bogus? */ + continue; + + *ret_hd = (HARDDRIVE_DEVICE_PATH) { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_HARDDRIVE_DP, + .Length = sizeof(HARDDRIVE_DEVICE_PATH), + }, + .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)); + + 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(const EFI_GUID *type, 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, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (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; !device_path_is_end(node); + node = device_path_next_node(node)) { + if (node->Type != MEDIA_DEVICE_PATH || node->SubType != 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 = device_path_replace_node(partition_path, part_node, NULL); + + EFI_HANDLE disk_handle; + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), &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, MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), (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 (size_t 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(type, 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 = device_path_replace_node(partition_path, part_node, (EFI_DEVICE_PATH *) &hd); + return EFI_SUCCESS; + } + + /* No xbootloader partition found */ + return EFI_NOT_FOUND; +} + +EFI_STATUS partition_open(const EFI_GUID *type, 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(type); + assert(device); + assert(ret_root_dir); + + err = find_device(type, device, &partition_path); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *dp = partition_path; + err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), &dp, &new_device); + if (err != EFI_SUCCESS) + return err; + + err = open_volume(new_device, &root_dir); + if (err != EFI_SUCCESS) + return err; + + if (ret_device) + *ret_device = new_device; + *ret_root_dir = root_dir; + return EFI_SUCCESS; +} + +char16_t *disk_get_part_uuid(EFI_HANDLE *handle) { + EFI_STATUS err; + EFI_DEVICE_PATH *dp; + + /* export the device path this image is started from */ + + if (!handle) + return NULL; + + err = BS->HandleProtocol(handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return NULL; + + for (; !device_path_is_end(dp); dp = device_path_next_node(dp)) { + if (dp->Type != MEDIA_DEVICE_PATH || dp->SubType != MEDIA_HARDDRIVE_DP) + continue; + + HARDDRIVE_DEVICE_PATH *hd = (HARDDRIVE_DEVICE_PATH *) dp; + if (hd->SignatureType != SIGNATURE_TYPE_GUID) + continue; + + return xasprintf(GUID_FORMAT_STR, GUID_FORMAT_VAL(hd->SignatureGuid)); + } + + return NULL; +} diff --git a/src/boot/efi/part-discovery.h b/src/boot/efi/part-discovery.h new file mode 100644 index 0000000..bbc87ff --- /dev/null +++ b/src/boot/efi/part-discovery.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define XBOOTLDR_GUID \ + { 0xbc13c2ff, 0x59e6, 0x4262, { 0xa3, 0x52, 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72 } } +#define ESP_GUID \ + { 0xc12a7328, 0xf81f, 0x11d2, { 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b } } + +EFI_STATUS partition_open(const EFI_GUID *type, EFI_HANDLE *device, EFI_HANDLE *ret_device, EFI_FILE **ret_root_dir); +char16_t *disk_get_part_uuid(EFI_HANDLE *handle); diff --git a/src/boot/efi/pe.c b/src/boot/efi/pe.c new file mode 100644 index 0000000..829266b --- /dev/null +++ b/src/boot/efi/pe.c @@ -0,0 +1,332 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#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 0x014CU +# define TARGET_MACHINE_TYPE_COMPATIBILITY 0x8664U +#elif defined(__x86_64__) +# define TARGET_MACHINE_TYPE 0x8664U +#elif defined(__aarch64__) +# define TARGET_MACHINE_TYPE 0xAA64U +#elif defined(__arm__) +# define TARGET_MACHINE_TYPE 0x01C2U +#elif defined(__riscv) && __riscv_xlen == 32 +# define TARGET_MACHINE_TYPE 0x5032U +#elif defined(__riscv) && __riscv_xlen == 64 +# define TARGET_MACHINE_TYPE 0x5064U +#elif defined(__loongarch__) && __loongarch_grlen == 32 +# define TARGET_MACHINE_TYPE 0x6232U +#elif defined(__loongarch__) && __loongarch_grlen == 64 +# define TARGET_MACHINE_TYPE 0x6264U +#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 bool verify_dos(const DosFileHeader *dos) { + assert(dos); + return memcmp(dos->Magic, DOS_FILE_MAGIC, STRLEN(DOS_FILE_MAGIC)) == 0; +} + +static 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 size_t 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[], + size_t n_table, + const char * const sections[], + size_t *offsets, + size_t *sizes, + bool in_memory) { + + assert(section_table); + assert(sections); + assert(offsets); + assert(sizes); + + for (size_t i = 0; i < n_table; i++) { + const PeSectionHeader *sect = section_table + i; + + for (size_t 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) { + size_t 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 64-bit kernel on 32-bit EFI that is otherwise running on a 64-bit 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[], size_t *addrs, size_t *sizes) { + const DosFileHeader *dos; + const PeFileHeader *pe; + size_t 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[], + size_t *offsets, + size_t *sizes) { + _cleanup_free_ PeSectionHeader *section_table = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + DosFileHeader dos; + PeFileHeader pe; + size_t 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..7e2258f --- /dev/null +++ b/src/boot/efi/pe.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS pe_memory_locate_sections( + const void *base, + const char * const sections[], + size_t *addrs, + size_t *sizes); + +EFI_STATUS pe_file_locate_sections( + EFI_FILE *dir, + const char16_t *path, + const char * const sections[], + size_t *offsets, + size_t *sizes); + +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address); diff --git a/src/boot/efi/proto/block-io.h b/src/boot/efi/proto/block-io.h new file mode 100644 index 0000000..e977f70 --- /dev/null +++ b/src/boot/efi/proto/block-io.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_BLOCK_IO_PROTOCOL_GUID \ + GUID_DEF(0x0964e5b21, 0x6459, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +typedef struct EFI_BLOCK_IO_PROTOCOL EFI_BLOCK_IO_PROTOCOL; +struct EFI_BLOCK_IO_PROTOCOL { + uint64_t Revision; + struct { + uint32_t MediaId; + bool RemovableMedia; + bool MediaPresent; + bool LogicalPartition; + bool ReadOnly; + bool WriteCaching; + uint32_t BlockSize; + uint32_t IoAlign; + EFI_LBA LastBlock; + EFI_LBA LowestAlignedLba; + uint32_t LogicalBlocksPerPhysicalBlock; + uint32_t OptimalTransferLengthGranularity; + } *Media; + + EFI_STATUS (EFIAPI *Reset)( + EFI_BLOCK_IO_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadBlocks)( + EFI_BLOCK_IO_PROTOCOL *This, + uint32_t MediaId, + EFI_LBA LBA, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *WriteBlocks)( + EFI_BLOCK_IO_PROTOCOL *This, + uint32_t MediaId, + EFI_LBA LBA, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *FlushBlocks)(EFI_BLOCK_IO_PROTOCOL *This); +}; diff --git a/src/boot/efi/proto/console-control.h b/src/boot/efi/proto/console-control.h new file mode 100644 index 0000000..d3a92ea --- /dev/null +++ b/src/boot/efi/proto/console-control.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_CONSOLE_CONTROL_PROTOCOL_GUID \ + GUID_DEF(0xf42f7782, 0x12e, 0x4c12, 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21) + +typedef enum { + EfiConsoleControlScreenText, + EfiConsoleControlScreenGraphics, + EfiConsoleControlScreenMaxValue, +} EFI_CONSOLE_CONTROL_SCREEN_MODE; + +typedef struct EFI_CONSOLE_CONTROL_PROTOCOL EFI_CONSOLE_CONTROL_PROTOCOL; +struct EFI_CONSOLE_CONTROL_PROTOCOL { + EFI_STATUS (EFIAPI *GetMode)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE *Mode, + bool *UgaExists, + bool *StdInLocked); + EFI_STATUS (EFIAPI *SetMode)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE Mode); + EFI_STATUS(EFIAPI *LockStdIn)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + char16_t *Password); +}; diff --git a/src/boot/efi/proto/device-path.h b/src/boot/efi/proto/device-path.h new file mode 100644 index 0000000..0fabae1 --- /dev/null +++ b/src/boot/efi/proto/device-path.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_DEVICE_PATH_PROTOCOL_GUID \ + GUID_DEF(0x09576e91, 0x6d3f, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_DEVICE_PATH_TO_TEXT_PROTOCOL_GUID \ + GUID_DEF(0x8b843e20, 0x8132, 0x4852, 0x90, 0xcc, 0x55, 0x1a, 0x4e, 0x4a, 0x7f, 0x1c) +#define EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL_GUID \ + GUID_DEF(0x05c99a21, 0xc70f, 0x4ad2, 0x8a, 0x5f, 0x35, 0xdf, 0x33, 0x43, 0xf5, 0x1e) + +/* Device path types. */ +enum { + HARDWARE_DEVICE_PATH = 0x01, + ACPI_DEVICE_PATH = 0x02, + MESSAGING_DEVICE_PATH = 0x03, + MEDIA_DEVICE_PATH = 0x04, + BBS_DEVICE_PATH = 0x05, + END_DEVICE_PATH_TYPE = 0x7f, +}; + +/* Device path sub-types. */ +enum { + END_INSTANCE_DEVICE_PATH_SUBTYPE = 0x01, + END_ENTIRE_DEVICE_PATH_SUBTYPE = 0xff, + + MEDIA_HARDDRIVE_DP = 0x01, + MEDIA_VENDOR_DP = 0x03, + MEDIA_FILEPATH_DP = 0x04, + MEDIA_PIWG_FW_FILE_DP = 0x06, + MEDIA_PIWG_FW_VOL_DP = 0x07, +}; + +struct _packed_ EFI_DEVICE_PATH_PROTOCOL { + uint8_t Type; + uint8_t SubType; + uint16_t Length; +}; + +typedef struct { + EFI_DEVICE_PATH Header; + EFI_GUID Guid; +} _packed_ VENDOR_DEVICE_PATH; + +#define MBR_TYPE_PCAT 0x01U +#define MBR_TYPE_EFI_PARTITION_TABLE_HEADER 0x02U +#define NO_DISK_SIGNATURE 0x00U +#define SIGNATURE_TYPE_MBR 0x01U +#define SIGNATURE_TYPE_GUID 0x02U + +typedef struct { + EFI_DEVICE_PATH Header; + uint32_t PartitionNumber; + uint64_t PartitionStart; + uint64_t PartitionSize; + union { + uint8_t Signature[16]; + EFI_GUID SignatureGuid; + }; + uint8_t MBRType; + uint8_t SignatureType; +} _packed_ HARDDRIVE_DEVICE_PATH; + +typedef struct { + EFI_DEVICE_PATH Header; + char16_t PathName[]; +} _packed_ FILEPATH_DEVICE_PATH; + +typedef struct { + char16_t* (EFIAPI *ConvertDeviceNodeToText)( + const EFI_DEVICE_PATH *DeviceNode, + bool DisplayOnly, + bool AllowShortcuts); + char16_t* (EFIAPI *ConvertDevicePathToText)( + const EFI_DEVICE_PATH *DevicePath, + bool DisplayOnly, + bool AllowShortcuts); +} EFI_DEVICE_PATH_TO_TEXT_PROTOCOL; + +typedef struct { + EFI_DEVICE_PATH* (EFIAPI *ConvertTextToDevicNode)( + const char16_t *TextDeviceNode); + EFI_DEVICE_PATH* (EFIAPI *ConvertTextToDevicPath)( + const char16_t *ConvertTextToDevicPath); +} EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL; diff --git a/src/boot/efi/proto/dt-fixup.h b/src/boot/efi/proto/dt-fixup.h new file mode 100644 index 0000000..6edbef5 --- /dev/null +++ b/src/boot/efi/proto/dt-fixup.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_DTB_TABLE_GUID \ + GUID_DEF(0xb1b621d5, 0xf19c, 0x41a5, 0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0) +#define EFI_DT_FIXUP_PROTOCOL_GUID \ + GUID_DEF(0xe617d64c, 0xfe08, 0x46da, 0xf4, 0xdc, 0xbb, 0xd5, 0x87, 0x0c, 0x73, 0x00) + +#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; +struct EFI_DT_FIXUP_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *Fixup)( + EFI_DT_FIXUP_PROTOCOL *This, + void *Fdt, + size_t *BufferSize, + uint32_t Flags); +}; diff --git a/src/boot/efi/proto/file-io.h b/src/boot/efi/proto/file-io.h new file mode 100644 index 0000000..001ad48 --- /dev/null +++ b/src/boot/efi/proto/file-io.h @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \ + GUID_DEF(0x0964e5b22, 0x6459, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_FILE_INFO_ID \ + GUID_DEF(0x009576e92, 0x6d3f, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +#define EFI_FILE_MODE_READ 0x0000000000000001U +#define EFI_FILE_MODE_WRITE 0x0000000000000002U +#define EFI_FILE_MODE_CREATE 0x8000000000000000U + +#define EFI_FILE_READ_ONLY 0x01U +#define EFI_FILE_HIDDEN 0x02U +#define EFI_FILE_SYSTEM 0x04U +#define EFI_FILE_RESERVED 0x08U +#define EFI_FILE_DIRECTORY 0x10U +#define EFI_FILE_ARCHIVE 0x20U +#define EFI_FILE_VALID_ATTR 0x37U + +typedef struct { + uint64_t Size; + uint64_t FileSize; + uint64_t PhysicalSize; + EFI_TIME CreateTime; + EFI_TIME LastAccessTime; + EFI_TIME ModificationTime; + uint64_t Attribute; + char16_t FileName[]; +} EFI_FILE_INFO; + +/* 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, we always start with a buffer that should handle FAT32 + * max file name length. */ +#define EFI_FILE_INFO_MIN_SIZE (offsetof(EFI_FILE_INFO, FileName) + 256U * sizeof(char16_t)) + +typedef struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL EFI_SIMPLE_FILE_SYSTEM_PROTOCOL; +struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *OpenVolume)( + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This, + EFI_FILE **Root); +}; + +struct EFI_FILE_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *Open)( + EFI_FILE *This, + EFI_FILE **NewHandle, + char16_t *FileName, + uint64_t OpenMode, + uint64_t Attributes); + EFI_STATUS (EFIAPI *Close)(EFI_FILE *This); + EFI_STATUS (EFIAPI *Delete)(EFI_FILE *This); + EFI_STATUS (EFIAPI *Read)( + EFI_FILE *This, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *Write)( + EFI_FILE *This, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *GetPosition)(EFI_FILE *This, uint64_t *Position); + EFI_STATUS (EFIAPI *SetPosition)(EFI_FILE *This, uint64_t Position); + EFI_STATUS (EFIAPI *GetInfo)( + EFI_FILE *This, + EFI_GUID *InformationType, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *SetInfo)( + EFI_FILE *This, + EFI_GUID *InformationType, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *Flush)(EFI_FILE *This); + void *OpenEx; + void *ReadEx; + void *WriteEx; + void *FlushEx; +}; diff --git a/src/boot/efi/proto/graphics-output.h b/src/boot/efi/proto/graphics-output.h new file mode 100644 index 0000000..f49e580 --- /dev/null +++ b/src/boot/efi/proto/graphics-output.h @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \ + GUID_DEF(0x9042a9de, 0x23dc, 0x4a38, 0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a) + +typedef enum { + PixelRedGreenBlueReserved8BitPerColor, + PixelBlueGreenRedReserved8BitPerColor, + PixelBitMask, + PixelBltOnly, + PixelFormatMax, +} EFI_GRAPHICS_PIXEL_FORMAT; + +typedef enum { + EfiBltVideoFill, + EfiBltVideoToBltBuffer, + EfiBltBufferToVideo, + EfiBltVideoToVideo, + EfiGraphicsOutputBltOperationMax, +} EFI_GRAPHICS_OUTPUT_BLT_OPERATION; + +typedef struct { + uint32_t RedMask; + uint32_t GreenMask; + uint32_t BlueMask; + uint32_t ReservedMask; +} EFI_PIXEL_BITMASK; + +typedef struct { + uint8_t Blue; + uint8_t Green; + uint8_t Red; + uint8_t Reserved; +} EFI_GRAPHICS_OUTPUT_BLT_PIXEL; + +typedef struct { + uint32_t Version; + uint32_t HorizontalResolution; + uint32_t VerticalResolution; + EFI_GRAPHICS_PIXEL_FORMAT PixelFormat; + EFI_PIXEL_BITMASK PixelInformation; + uint32_t PixelsPerScanLine; +} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION; + +typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL EFI_GRAPHICS_OUTPUT_PROTOCOL; +struct EFI_GRAPHICS_OUTPUT_PROTOCOL { + EFI_STATUS (EFIAPI *QueryMode)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + uint32_t ModeNumber, + size_t *SizeOfInfo, + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info); + EFI_STATUS(EFIAPI *SetMode)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + uint32_t ModeNumber); + EFI_STATUS (EFIAPI *Blt)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer, + EFI_GRAPHICS_OUTPUT_BLT_OPERATION BltOperation, + size_t SourceX, + size_t SourceY, + size_t DestinationX, + size_t DestinationY, + size_t Width, + size_t Height, + size_t Delta); + + struct { + uint32_t MaxMode; + uint32_t Mode; + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info; + size_t SizeOfInfo; + EFI_PHYSICAL_ADDRESS FrameBufferBase; + size_t FrameBufferSize; + } *Mode; +}; diff --git a/src/boot/efi/proto/load-file.h b/src/boot/efi/proto/load-file.h new file mode 100644 index 0000000..2e01ce5 --- /dev/null +++ b/src/boot/efi/proto/load-file.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_LOAD_FILE_PROTOCOL_GUID \ + GUID_DEF(0x56EC3091, 0x954C, 0x11d2, 0x8e, 0x3f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_LOAD_FILE2_PROTOCOL_GUID \ + GUID_DEF(0x4006c0c1, 0xfcb3, 0x403e, 0x99, 0x6d, 0x4a, 0x6c, 0x87, 0x24, 0xe0, 0x6d) + +typedef struct EFI_LOAD_FILE_PROTOCOL EFI_LOAD_FILE_PROTOCOL; +typedef EFI_LOAD_FILE_PROTOCOL EFI_LOAD_FILE2_PROTOCOL; + +struct EFI_LOAD_FILE_PROTOCOL { + EFI_STATUS (EFIAPI *LoadFile)( + EFI_LOAD_FILE_PROTOCOL *This, + EFI_DEVICE_PATH *FilePath, + bool BootPolicy, + size_t *BufferSize, + void *Buffer); +}; diff --git a/src/boot/efi/proto/loaded-image.h b/src/boot/efi/proto/loaded-image.h new file mode 100644 index 0000000..46371e7 --- /dev/null +++ b/src/boot/efi/proto/loaded-image.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_LOADED_IMAGE_PROTOCOL_GUID \ + GUID_DEF(0x5B1B31A1, 0x9562, 0x11d2, 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B) +#define EFI_LOADED_IMAGE_DEVICE_PATH_PROTOCOL_GUID \ + GUID_DEF(0xbc62157e, 0x3e33, 0x4fec, 0x99, 0x20, 0x2d, 0x3b, 0x36, 0xd7, 0x50, 0xdf) + +typedef EFI_STATUS (EFIAPI *EFI_IMAGE_ENTRY_POINT)( + EFI_HANDLE ImageHandle, + EFI_SYSTEM_TABLE *SystemTable); + +typedef struct { + uint32_t Revision; + EFI_HANDLE ParentHandle; + EFI_SYSTEM_TABLE *SystemTable; + EFI_HANDLE DeviceHandle; + EFI_DEVICE_PATH *FilePath; + void *Reserved; + uint32_t LoadOptionsSize; + void *LoadOptions; + void *ImageBase; + uint64_t ImageSize; + EFI_MEMORY_TYPE ImageCodeType; + EFI_MEMORY_TYPE ImageDataType; + EFI_STATUS (EFIAPI *Unload)(EFI_HANDLE ImageHandle); +} EFI_LOADED_IMAGE_PROTOCOL; diff --git a/src/boot/efi/proto/rng.h b/src/boot/efi/proto/rng.h new file mode 100644 index 0000000..8ed1fd4 --- /dev/null +++ b/src/boot/efi/proto/rng.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_RNG_PROTOCOL_GUID \ + GUID_DEF(0x3152bca5, 0xeade, 0x433d, 0x86, 0x2e, 0xc0, 0x1c, 0xdc, 0x29, 0x1f, 0x44) + +typedef struct EFI_RNG_PROTOCOL EFI_RNG_PROTOCOL; +struct EFI_RNG_PROTOCOL { + EFI_STATUS (EFIAPI *GetInfo)( + EFI_RNG_PROTOCOL *This, + size_t *RNGAlgorithmListSize, + EFI_GUID *RNGAlgorithmList); + EFI_STATUS (EFIAPI *GetRNG)( + EFI_RNG_PROTOCOL *This, + EFI_GUID *RNGAlgorithm, + size_t RNGValueLength, + uint8_t *RNGValue); +}; diff --git a/src/boot/efi/proto/security-arch.h b/src/boot/efi/proto/security-arch.h new file mode 100644 index 0000000..2675c61 --- /dev/null +++ b/src/boot/efi/proto/security-arch.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SECURITY_ARCH_PROTOCOL_GUID \ + GUID_DEF(0xA46423E3, 0x4617, 0x49f1, 0xB9, 0xFF, 0xD1, 0xBF, 0xA9, 0x11, 0x58, 0x39) +#define EFI_SECURITY2_ARCH_PROTOCOL_GUID \ + GUID_DEF(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); + +typedef EFI_STATUS (EFIAPI *EFI_SECURITY2_FILE_AUTHENTICATION)( + const EFI_SECURITY2_ARCH_PROTOCOL *This, + const EFI_DEVICE_PATH *DevicePath, + void *FileBuffer, + size_t FileSize, + bool BootPolicy); + +struct EFI_SECURITY_ARCH_PROTOCOL { + EFI_SECURITY_FILE_AUTHENTICATION_STATE FileAuthenticationState; +}; + +struct EFI_SECURITY2_ARCH_PROTOCOL { + EFI_SECURITY2_FILE_AUTHENTICATION FileAuthentication; +}; diff --git a/src/boot/efi/proto/shell-parameters.h b/src/boot/efi/proto/shell-parameters.h new file mode 100644 index 0000000..8080922 --- /dev/null +++ b/src/boot/efi/proto/shell-parameters.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SHELL_PARAMETERS_PROTOCOL_GUID \ + GUID_DEF(0x752f3136, 0x4e16, 0x4fdc, 0xa2, 0x2a, 0xe5, 0xf4, 0x68, 0x12, 0xf4, 0xca) + +typedef struct { + char16_t **Argv; + size_t Argc; + void *StdIn; + void *StdOut; + void *StdErr; +} EFI_SHELL_PARAMETERS_PROTOCOL; diff --git a/src/boot/efi/proto/simple-text-io.h b/src/boot/efi/proto/simple-text-io.h new file mode 100644 index 0000000..95016d3 --- /dev/null +++ b/src/boot/efi/proto/simple-text-io.h @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID \ + GUID_DEF(0x387477c1, 0x69c7, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \ + GUID_DEF(0xdd9e7534, 0x7762, 0x4698, 0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa) +#define EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID \ + GUID_DEF(0x387477c2, 0x69c7, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +#define EFI_SHIFT_STATE_VALID 0x80000000U +#define EFI_RIGHT_SHIFT_PRESSED 0x00000001U +#define EFI_LEFT_SHIFT_PRESSED 0x00000002U +#define EFI_RIGHT_CONTROL_PRESSED 0x00000004U +#define EFI_LEFT_CONTROL_PRESSED 0x00000008U +#define EFI_RIGHT_ALT_PRESSED 0x00000010U +#define EFI_LEFT_ALT_PRESSED 0x00000020U +#define EFI_RIGHT_LOGO_PRESSED 0x00000040U +#define EFI_LEFT_LOGO_PRESSED 0x00000080U +#define EFI_MENU_KEY_PRESSED 0x00000100U +#define EFI_SYS_REQ_PRESSED 0x00000200U + +#define EFI_TOGGLE_STATE_VALID 0x80U +#define EFI_KEY_STATE_EXPOSED 0x40U +#define EFI_SCROLL_LOCK_ACTIVE 0x01U +#define EFI_NUM_LOCK_ACTIVE 0x02U +#define EFI_CAPS_LOCK_ACTIVE 0x04U + +enum { + EFI_BLACK = 0x00, + EFI_BLUE = 0x01, + EFI_GREEN = 0x02, + EFI_CYAN = EFI_BLUE | EFI_GREEN, + EFI_RED = 0x04, + EFI_MAGENTA = EFI_BLUE | EFI_RED, + EFI_BROWN = EFI_GREEN | EFI_RED, + EFI_LIGHTGRAY = EFI_BLUE | EFI_GREEN | EFI_RED, + EFI_BRIGHT = 0x08, + EFI_DARKGRAY = EFI_BLACK | EFI_BRIGHT, + EFI_LIGHTBLUE = EFI_BLUE | EFI_BRIGHT, + EFI_LIGHTGREEN = EFI_GREEN | EFI_BRIGHT, + EFI_LIGHTCYAN = EFI_CYAN | EFI_BRIGHT, + EFI_LIGHTRED = EFI_RED | EFI_BRIGHT, + EFI_LIGHTMAGENTA = EFI_MAGENTA | EFI_BRIGHT, + EFI_YELLOW = EFI_BROWN | EFI_BRIGHT, + EFI_WHITE = EFI_BLUE | EFI_GREEN | EFI_RED | EFI_BRIGHT, +}; + +#define EFI_TEXT_ATTR(fg, bg) ((fg) | ((bg) << 4)) +#define EFI_TEXT_ATTR_SWAP(c) EFI_TEXT_ATTR(((c) & 0xF0U) >> 4, (c) & 0xFU) + +enum { + SCAN_NULL = 0x000, + SCAN_UP = 0x001, + SCAN_DOWN = 0x002, + SCAN_RIGHT = 0x003, + SCAN_LEFT = 0x004, + SCAN_HOME = 0x005, + SCAN_END = 0x006, + SCAN_INSERT = 0x007, + SCAN_DELETE = 0x008, + SCAN_PAGE_UP = 0x009, + SCAN_PAGE_DOWN = 0x00A, + SCAN_F1 = 0x00B, + SCAN_F2 = 0x00C, + SCAN_F3 = 0x00D, + SCAN_F4 = 0x00E, + SCAN_F5 = 0x00F, + SCAN_F6 = 0x010, + SCAN_F7 = 0x011, + SCAN_F8 = 0x012, + SCAN_F9 = 0x013, + SCAN_F10 = 0x014, + SCAN_F11 = 0x015, + SCAN_F12 = 0x016, + SCAN_ESC = 0x017, + SCAN_PAUSE = 0x048, + SCAN_F13 = 0x068, + SCAN_F14 = 0x069, + SCAN_F15 = 0x06A, + SCAN_F16 = 0x06B, + SCAN_F17 = 0x06C, + SCAN_F18 = 0x06D, + SCAN_F19 = 0x06E, + SCAN_F20 = 0x06F, + SCAN_F21 = 0x070, + SCAN_F22 = 0x071, + SCAN_F23 = 0x072, + SCAN_F24 = 0x073, + SCAN_MUTE = 0x07F, + SCAN_VOLUME_UP = 0x080, + SCAN_VOLUME_DOWN = 0x081, + SCAN_BRIGHTNESS_UP = 0x100, + SCAN_BRIGHTNESS_DOWN = 0x101, + SCAN_SUSPEND = 0x102, + SCAN_HIBERNATE = 0x103, + SCAN_TOGGLE_DISPLAY = 0x104, + SCAN_RECOVERY = 0x105, + SCAN_EJECT = 0x106, +}; + +typedef struct { + uint16_t ScanCode; + char16_t UnicodeChar; +} EFI_INPUT_KEY; + +typedef struct { + uint32_t KeyShiftState; + uint8_t KeyToggleState; +} EFI_KEY_STATE; + +typedef struct { + EFI_INPUT_KEY Key; + EFI_KEY_STATE KeyState; +} EFI_KEY_DATA; + +struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadKeyStroke)( + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, + EFI_INPUT_KEY *Key); + EFI_EVENT WaitForKey; +}; + +typedef struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; +struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadKeyStrokeEx)( + EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + EFI_KEY_DATA *KeyData); + EFI_EVENT WaitForKeyEx; + void *SetState; + void *RegisterKeyNotify; + void *UnregisterKeyNotify; +}; + +typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; +struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *OutputString)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + char16_t *String); + EFI_STATUS (EFIAPI *TestString)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + char16_t *String); + EFI_STATUS (EFIAPI *QueryMode)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t ModeNumber, + size_t *Columns, + size_t *Rows); + EFI_STATUS (EFIAPI *SetMode)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t ModeNumber); + EFI_STATUS (EFIAPI *SetAttribute)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t Attribute); + EFI_STATUS (EFIAPI *ClearScreen)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This); + EFI_STATUS (EFIAPI *SetCursorPosition)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t Column, + size_t Row); + EFI_STATUS (EFIAPI *EnableCursor)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + bool Visible); + struct { + int32_t MaxMode; + int32_t Mode; + int32_t Attribute; + int32_t CursorColumn; + int32_t CursorRow; + bool CursorVisible; + } *Mode; +}; diff --git a/src/boot/efi/proto/tcg.h b/src/boot/efi/proto/tcg.h new file mode 100644 index 0000000..b4b8296 --- /dev/null +++ b/src/boot/efi/proto/tcg.h @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_TCG_PROTOCOL_GUID \ + GUID_DEF(0xf541796d, 0xa62e, 0x4954, 0xa7, 0x75, 0x95, 0x84, 0xf6, 0x1b, 0x9c, 0xdd) +#define EFI_TCG2_PROTOCOL_GUID \ + GUID_DEF(0x607f766c, 0x7455, 0x42be, 0x93, 0x0b, 0xe4, 0xd7, 0x6d, 0xb2, 0x72, 0x0f) + +#define TCG_ALG_SHA 0x4 +#define EFI_TCG2_EVENT_HEADER_VERSION 1 +#define EV_IPL 13 +#define EV_EVENT_TAG UINT32_C(6) + +typedef struct { + uint8_t Major; + uint8_t Minor; + uint8_t RevMajor; + uint8_t RevMinor; +} TCG_VERSION; + +typedef struct { + uint8_t Major; + uint8_t Minor; +} EFI_TCG2_VERSION; + +typedef struct { + uint8_t Size; + TCG_VERSION StructureVersion; + TCG_VERSION ProtocolSpecVersion; + uint8_t HashAlgorithmBitmap; + bool TPMPresentFlag; + bool TPMDeactivatedFlag; +} EFI_TCG_BOOT_SERVICE_CAPABILITY; + +typedef struct { + uint8_t Size; + EFI_TCG2_VERSION StructureVersion; + EFI_TCG2_VERSION ProtocolVersion; + uint32_t HashAlgorithmBitmap; + uint32_t SupportedEventLogs; + bool TPMPresentFlag; + uint16_t MaxCommandSize; + uint16_t MaxResponseSize; + uint32_t ManufacturerID; + uint32_t NumberOfPCRBanks; + uint32_t ActivePcrBanks; +} EFI_TCG2_BOOT_SERVICE_CAPABILITY; + +typedef struct { + uint32_t PCRIndex; + uint32_t EventType; + struct { + uint8_t Digest[20]; + } Digest; + uint32_t EventSize; + uint8_t Event[]; +} _packed_ TCG_PCR_EVENT; + +typedef struct { + uint32_t HeaderSize; + uint16_t HeaderVersion; + uint32_t PCRIndex; + uint32_t EventType; +} _packed_ EFI_TCG2_EVENT_HEADER; + +typedef struct { + uint32_t Size; + EFI_TCG2_EVENT_HEADER Header; + uint8_t Event[]; +} _packed_ EFI_TCG2_EVENT; + +typedef struct { + uint32_t EventId; + uint32_t EventSize; + uint8_t Event[]; +} _packed_ EFI_TCG2_TAGGED_EVENT; + +typedef struct EFI_TCG_PROTOCOL EFI_TCG_PROTOCOL; +struct EFI_TCG_PROTOCOL { + EFI_STATUS (EFIAPI *StatusCheck)( + EFI_TCG_PROTOCOL *This, + EFI_TCG_BOOT_SERVICE_CAPABILITY *ProtocolCapability, + uint32_t *TCGFeatureFlags, + EFI_PHYSICAL_ADDRESS *EventLogLocation, + EFI_PHYSICAL_ADDRESS *EventLogLastEntry); + void *HashAll; + void *LogEvent; + void *PassThroughToTpm; + EFI_STATUS (EFIAPI *HashLogExtendEvent)( + EFI_TCG_PROTOCOL *This, + EFI_PHYSICAL_ADDRESS HashData, + uint64_t HashDataLen, + uint32_t AlgorithmId, + TCG_PCR_EVENT *TCGLogData, + uint32_t *EventNumber, + EFI_PHYSICAL_ADDRESS *EventLogLastEntry); +}; + +typedef struct EFI_TCG2_PROTOCOL EFI_TCG2_PROTOCOL; +struct EFI_TCG2_PROTOCOL { + EFI_STATUS (EFIAPI *GetCapability)( + EFI_TCG2_PROTOCOL *This, + EFI_TCG2_BOOT_SERVICE_CAPABILITY *ProtocolCapability); + void *GetEventLog; + EFI_STATUS (EFIAPI *HashLogExtendEvent)( + EFI_TCG2_PROTOCOL *This, + uint64_t Flags, + EFI_PHYSICAL_ADDRESS DataToHash, + uint64_t DataToHashLen, + EFI_TCG2_EVENT *EfiTcgEvent); + void *SubmitCommand; + void *GetActivePcrBanks; + void *SetActivePcrBanks; + void *GetResultOfSetActivePcrBanks; +}; diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c new file mode 100644 index 0000000..8147e54 --- /dev/null +++ b/src/boot/efi/random-seed.c @@ -0,0 +1,325 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memory-util-fundamental.h" +#include "proto/rng.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) + +struct linux_efi_random_seed { + uint32_t size; + uint8_t seed[]; +}; + +#define LINUX_EFI_RANDOM_SEED_TABLE_GUID \ + { 0x1ce1e5bc, 0x7ceb, 0x42f2, { 0x81, 0xe5, 0x8a, 0xad, 0xf1, 0x80, 0xf5, 0x7b } } + +/* SHA256 gives us 256/8=32 bytes */ +#define HASH_VALUE_SIZE 32 + +/* Linux's RNG is 256 bits, so let's provide this much */ +#define DESIRED_SEED_SIZE 32 + +/* Some basic domain separation in case somebody uses this data elsewhere */ +#define HASH_LABEL "systemd-boot random seed label v1" + +static EFI_STATUS acquire_rng(void *ret, size_t size) { + EFI_RNG_PROTOCOL *rng; + EFI_STATUS err; + + assert(ret); + + /* Try to acquire the specified number of bytes from the UEFI RNG */ + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_RNG_PROTOCOL), NULL, (void **) &rng); + if (err != EFI_SUCCESS) + return err; + if (!rng) + return EFI_UNSUPPORTED; + + err = rng->GetRNG(rng, NULL, size, ret); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to acquire RNG data: %m"); + return EFI_SUCCESS; +} + +static EFI_STATUS acquire_system_token(void **ret, size_t *ret_size) { + _cleanup_free_ char *data = NULL; + EFI_STATUS err; + size_t size; + + assert(ret); + assert(ret_size); + + err = efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderSystemToken", &data, &size); + if (err != EFI_SUCCESS) { + if (err != EFI_NOT_FOUND) + log_error_status(err, "Failed to read LoaderSystemToken EFI variable: %m"); + return err; + } + + if (size <= 0) + return log_error_status(EFI_NOT_FOUND, "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 (size_t 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) { + uint8_t random_bytes[DESIRED_SEED_SIZE], hash_key[HASH_VALUE_SIZE]; + _cleanup_free_ struct linux_efi_random_seed *new_seed_table = NULL; + struct linux_efi_random_seed *previous_seed_table = NULL; + _cleanup_free_ void *seed = NULL, *system_token = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + _cleanup_free_ EFI_FILE_INFO *info = NULL; + struct sha256_ctx hash; + uint64_t uefi_monotonic_counter = 0; + size_t size, rsize, wsize; + bool seeded_by_efi = false; + EFI_STATUS err; + EFI_TIME now; + + CLEANUP_ERASE(random_bytes); + CLEANUP_ERASE(hash_key); + CLEANUP_ERASE(hash); + + assert(root_dir); + assert_cc(DESIRED_SEED_SIZE == HASH_VALUE_SIZE); + + validate_sha256(); + + /* hash = LABEL || sizeof(input1) || input1 || ... || sizeof(inputN) || inputN */ + sha256_init_ctx(&hash); + + /* Some basic domain separation in case somebody uses this data elsewhere */ + sha256_process_bytes(HASH_LABEL, sizeof(HASH_LABEL) - 1, &hash); + + previous_seed_table = find_configuration_table(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE)); + if (!previous_seed_table) { + size = 0; + sha256_process_bytes(&size, sizeof(size), &hash); + } else { + size = previous_seed_table->size; + seeded_by_efi = size >= DESIRED_SEED_SIZE; + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(previous_seed_table->seed, size, &hash); + + /* Zero and free the previous seed table only at the end after we've managed to install a new + * one, so that in case this function fails or aborts, Linux still receives whatever the + * previous bootloader chain set. So, the next line of this block is not an explicit_bzero() + * call. */ + } + + /* 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. */ + err = acquire_rng(random_bytes, sizeof(random_bytes)); + if (err != EFI_SUCCESS) { + size = 0; + /* If we can't get any randomness from EFI itself, then we'll only be relying on what's in + * ESP. But ESP is mutable, so if secure boot is enabled, we probably shouldn't trust that + * alone, in which case we bail out early. */ + if (!seeded_by_efi && secure_boot_enabled()) + return EFI_NOT_FOUND; + } else { + seeded_by_efi = true; + size = sizeof(random_bytes); + } + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(random_bytes, size, &hash); + + /* 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. */ + size = 0; + err = acquire_system_token(&system_token, &size); + if ((err != EFI_SUCCESS || size < DESIRED_SEED_SIZE) && !seeded_by_efi) + return err; + sha256_process_bytes(&size, sizeof(size), &hash); + if (system_token) { + sha256_process_bytes(system_token, size, &hash); + explicit_bzero_safe(system_token, size); + } + + err = root_dir->Open( + root_dir, + &handle, + (char16_t *) u"\\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_status(err, "Failed to open random seed file: %m"); + return err; + } + + err = get_file_info(handle, &info, NULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to get file info for random seed: %m"); + + size = info->FileSize; + if (size < RANDOM_MAX_SIZE_MIN) + return log_error("Random seed file is too short."); + + if (size > RANDOM_MAX_SIZE_MAX) + return log_error("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(err, "Failed to read random seed file: %m"); + if (rsize != size) { + explicit_bzero_safe(seed, rsize); + return log_error_status(EFI_PROTOCOL_ERROR, "Short read on random seed file."); + } + + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(seed, size, &hash); + explicit_bzero_safe(seed, size); + + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + + /* 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 && !seeded_by_efi) + return log_error_status(err, "Failed to acquire UEFI monotonic counter: %m"); + size = sizeof(uefi_monotonic_counter); + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&uefi_monotonic_counter, size, &hash); + + err = RT->GetTime(&now, NULL); + size = err == EFI_SUCCESS ? sizeof(now) : 0; /* Known to be flaky, so don't bark on error. */ + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&now, size, &hash); + + /* hash_key = HASH(hash) */ + sha256_finish_ctx(&hash, hash_key); + + /* hash = hash_key || 0 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 0 }, sizeof(uint8_t), &hash); + /* random_bytes = HASH(hash) */ + sha256_finish_ctx(&hash, random_bytes); + + size = sizeof(random_bytes); + /* If the file size is too large, zero out the remaining bytes on disk. */ + if (size < info->FileSize) { + err = handle->SetPosition(handle, size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to offset of random seed file: %m"); + wsize = info->FileSize - size; + err = handle->Write(handle, &wsize, seed /* All zeros now */); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to write random seed file: %m"); + if (wsize != info->FileSize - size) + return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file."); + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to flush random seed file: %m"); + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + + /* We could truncate the file here with something like: + * + * info->FileSize = size; + * err = handle->SetInfo(handle, &GenericFileInfo, info->Size, info); + * if (err != EFI_SUCCESS) + * return log_error_status(err, "Failed to truncate random seed file: %u"); + * + * But this is considered slightly risky, because EFI filesystem drivers are a little bit + * flimsy. So instead we rely on userspace eventually truncating this when it writes a new + * seed. For now the best we do is zero it. */ + } + /* Update the random seed on disk before we use it */ + wsize = size; + err = handle->Write(handle, &wsize, random_bytes); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to write random seed file: %m"); + if (wsize != size) + return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file."); + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to flush random seed file: %m"); + + err = BS->AllocatePool(EfiACPIReclaimMemory, + offsetof(struct linux_efi_random_seed, seed) + DESIRED_SEED_SIZE, + (void **) &new_seed_table); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to allocate EFI table for random seed: %m"); + new_seed_table->size = DESIRED_SEED_SIZE; + + /* hash = hash_key || 1 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 1 }, sizeof(uint8_t), &hash); + /* new_seed_table->seed = HASH(hash) */ + sha256_finish_ctx(&hash, new_seed_table->seed); + + err = BS->InstallConfigurationTable(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE), new_seed_table); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to install EFI table for random seed: %m"); + TAKE_PTR(new_seed_table); + + if (previous_seed_table) { + /* Now that we've succeeded in installing the new table, we can safely nuke the old one. */ + explicit_bzero_safe(previous_seed_table->seed, previous_seed_table->size); + explicit_bzero_safe(previous_seed_table, sizeof(*previous_seed_table)); + free(previous_seed_table); + } + + 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..67f005d --- /dev/null +++ b/src/boot/efi/random-seed.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS process_random_seed(EFI_FILE *root_dir); diff --git a/src/boot/efi/secure-boot.c b/src/boot/efi/secure-boot.c new file mode 100644 index 0000000..f76d2f4 --- /dev/null +++ b/src/boot/efi/secure-boot.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "console.h" +#include "proto/security-arch.h" +#include "secure-boot.h" +#include "util.h" +#include "vmm.h" + +bool secure_boot_enabled(void) { + bool secure = false; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + err = efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"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(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"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(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"AuditMode", &audit); + (void) efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"DeployedMode", &deployed); + (void) efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"SetupMode", &setup); + + return decode_secure_boot_mode(secure, audit, deployed, setup); +} + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path, bool force) { + assert(root_dir); + assert(path); + + EFI_STATUS err; + + clear_screen(COLOR_NORMAL); + + /* Enrolling secure boot keys is safe to do in virtualized environments as there is nothing + * we can brick there. */ + bool is_safe = in_hypervisor(); + + if (!is_safe && !force) + return EFI_SUCCESS; + + printf("Enrolling secure boot keys from directory: %ls\n", path); + + if (!is_safe) { + printf("Warning: Enrolling custom Secure Boot keys might soft-brick your machine!\n"); + + unsigned timeout_sec = 15; + for (;;) { + printf("\rEnrolling 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( + err, + "Error waiting for user input to enroll Secure Boot keys: %m"); + + /* user aborted, returning EFI_SUCCESS here allows the user to go back to the menu */ + return EFI_SUCCESS; + } + + printf("\n"); + } + + _cleanup_(file_closep) EFI_FILE *dir = NULL; + + err = open_directory(root_dir, path, &dir); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed opening keys directory %ls: %m", path); + + 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_GUID, 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_status(err, "Failed reading file %ls\\%ls: %m", path, sb_vars[i].filename); + 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_status(err, "Failed to write %ls secure boot variable: %m", sb_vars[i].name); + goto out_deallocate; + } + } + + printf("Custom Secure Boot keys successfully enrolled, rebooting the system now!\n"); + /* 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++) + free(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, + bool 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(MAKE_GUID_PTR(EFI_SECURITY_ARCH_PROTOCOL), 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(MAKE_GUID_PTR(EFI_SECURITY2_ARCH_PROTOCOL), 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..3471131 --- /dev/null +++ b/src/boot/efi/secure-boot.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "efivars-fundamental.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_IF_SAFE, /* Automatically enroll if it is safe (if we are running inside a VM, for example). */ + 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, bool force); + +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..df136ed --- /dev/null +++ b/src/boot/efi/shim.c @@ -0,0 +1,108 @@ +/* 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 "device-path-util.h" +#include "secure-boot.h" +#include "shim.h" +#include "util.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 \ + { 0x605dab50, 0xe046, 0x4300, { 0xab, 0xb6, 0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23 } } + +bool shim_loaded(void) { + struct ShimLock *shim_lock; + + return BS->LocateProtocol(MAKE_GUID_PTR(SHIM_LOCK), 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( + MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), &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(MAKE_GUID_PTR(SHIM_LOCK), 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; +} + +void shim_retain_protocol(void) { + uint8_t value = 1; + + /* Ask Shim to avoid uninstalling its security protocol, so that we can use it from sd-stub to + * validate PE addons. By default, Shim uninstalls its protocol when calling StartImage(). + * Requires Shim 15.8. */ + (void) efivar_set_raw(MAKE_GUID_PTR(SHIM_LOCK), u"ShimRetainProtocol", &value, sizeof(value), 0); +} diff --git a/src/boot/efi/shim.h b/src/boot/efi/shim.h new file mode 100644 index 0000000..e0cb39f --- /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" + +bool shim_loaded(void); +EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image); +void shim_retain_protocol(void); diff --git a/src/boot/efi/splash.c b/src/boot/efi/splash.c new file mode 100644 index 0000000..8daeb71 --- /dev/null +++ b/src/boot/efi/splash.c @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "graphics.h" +#include "logarithm.h" +#include "proto/graphics-output.h" +#include "splash.h" +#include "unaligned-fundamental.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; + uint32_t channel_mask_r; + uint32_t channel_mask_g; + uint32_t channel_mask_b; + uint32_t channel_mask_a; +} _packed_; + +#define SIZEOF_BMP_DIB offsetof(struct bmp_dib, channel_mask_r) +#define SIZEOF_BMP_DIB_RGB offsetof(struct bmp_dib, channel_mask_a) +#define SIZEOF_BMP_DIB_RGBA sizeof(struct bmp_dib) + +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, + size_t size, + struct bmp_dib **ret_dib, + struct bmp_map **ret_map, + const uint8_t **pixmap) { + + assert(bmp); + assert(ret_dib); + assert(ret_map); + assert(pixmap); + + if (size < sizeof(struct bmp_file) + SIZEOF_BMP_DIB) + return EFI_INVALID_PARAMETER; + + /* check file header */ + struct bmp_file *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 */ + struct bmp_dib *dib = (struct bmp_dib *) (bmp + sizeof(struct bmp_file)); + if (dib->size < SIZEOF_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; + } + + size_t row_size = ((size_t) 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 */ + struct bmp_map *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 = 0; + + if (dib->colors_used) + map_count = dib->colors_used; + else if (IN_SET(dib->depth, 1, 4, 8)) + map_count = 1 << dib->depth; + + size_t 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; +} + +enum Channels { R, G, B, A, _CHANNELS_MAX }; +static void read_channel_maks( + const struct bmp_dib *dib, + uint32_t channel_mask[static _CHANNELS_MAX], + uint8_t channel_shift[static _CHANNELS_MAX], + uint8_t channel_scale[static _CHANNELS_MAX]) { + + assert(dib); + + if (IN_SET(dib->depth, 16, 32) && dib->size >= SIZEOF_BMP_DIB_RGB) { + channel_mask[R] = dib->channel_mask_r; + channel_mask[G] = dib->channel_mask_g; + channel_mask[B] = dib->channel_mask_b; + channel_shift[R] = __builtin_ctz(dib->channel_mask_r); + channel_shift[G] = __builtin_ctz(dib->channel_mask_g); + channel_shift[B] = __builtin_ctz(dib->channel_mask_b); + channel_scale[R] = 0xff / ((1 << popcount(dib->channel_mask_r)) - 1); + channel_scale[G] = 0xff / ((1 << popcount(dib->channel_mask_g)) - 1); + channel_scale[B] = 0xff / ((1 << popcount(dib->channel_mask_b)) - 1); + + if (dib->size >= SIZEOF_BMP_DIB_RGBA && dib->channel_mask_a != 0) { + channel_mask[A] = dib->channel_mask_a; + channel_shift[A] = __builtin_ctz(dib->channel_mask_a); + channel_scale[A] = 0xff / ((1 << popcount(dib->channel_mask_a)) - 1); + } else { + channel_mask[A] = 0; + channel_shift[A] = 0; + channel_scale[A] = 0; + } + } else { + bool bpp16 = dib->depth == 16; + channel_mask[R] = bpp16 ? 0x7C00 : 0xFF0000; + channel_mask[G] = bpp16 ? 0x03E0 : 0x00FF00; + channel_mask[B] = bpp16 ? 0x001F : 0x0000FF; + channel_mask[A] = bpp16 ? 0x0000 : 0x000000; + channel_shift[R] = bpp16 ? 0xA : 0x10; + channel_shift[G] = bpp16 ? 0x5 : 0x08; + channel_shift[B] = bpp16 ? 0x0 : 0x00; + channel_shift[A] = bpp16 ? 0x0 : 0x00; + channel_scale[R] = bpp16 ? 0x08 : 0x1; + channel_scale[G] = bpp16 ? 0x08 : 0x1; + channel_scale[B] = bpp16 ? 0x08 : 0x1; + channel_scale[A] = bpp16 ? 0x00 : 0x0; + } +} + +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); + + uint32_t channel_mask[_CHANNELS_MAX]; + uint8_t channel_shift[_CHANNELS_MAX], channel_scale[_CHANNELS_MAX]; + read_channel_maks(dib, channel_mask, channel_shift, channel_scale); + + /* transform and copy pixels */ + in = pixmap; + for (uint32_t y = 0; y < dib->y; y++) { + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *out = &buf[(dib->y - y - 1) * dib->x]; + + for (uint32_t x = 0; x < dib->x; x++, in++, out++) { + switch (dib->depth) { + case 1: { + for (unsigned 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: { + unsigned 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 24: + out->Red = in[2]; + out->Green = in[1]; + out->Blue = in[0]; + in += 2; + break; + + case 16: + case 32: { + uint32_t i = dib->depth == 16 ? unaligned_read_ne16(in) : + unaligned_read_ne32(in); + + uint8_t r = ((i & channel_mask[R]) >> channel_shift[R]) * channel_scale[R], + g = ((i & channel_mask[G]) >> channel_shift[G]) * channel_scale[G], + b = ((i & channel_mask[B]) >> channel_shift[B]) * channel_scale[B], + a = 0xFFu; + if (channel_mask[A] != 0) + a = ((i & channel_mask[A]) >> channel_shift[A]) * channel_scale[A]; + + out->Red = (out->Red * (0xFFu - a) + r * a) >> 8; + out->Green = (out->Green * (0xFFu - a) + g * a) >> 8; + out->Blue = (out->Blue * (0xFFu - a) + b * a) >> 8; + + in += dib->depth == 16 ? 1 : 3; + break; + } + } + } + + /* add row padding; new lines always start at 32 bit boundary */ + size_t row_size = in - pixmap; + in += ((row_size + 3) & ~3) - row_size; + } + + return EFI_SUCCESS; +} + +EFI_STATUS graphics_splash(const uint8_t *content, size_t 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; + size_t x_pos = 0, 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(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), 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; + + /* Read in current screen content to perform proper alpha blending. */ + _cleanup_free_ EFI_GRAPHICS_OUTPUT_BLT_PIXEL *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..a66eb24 --- /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, size_t len); diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c new file mode 100644 index 0000000..7ef3e76 --- /dev/null +++ b/src/boot/efi/stub.c @@ -0,0 +1,816 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "device-path-util.h" +#include "devicetree.h" +#include "graphics.h" +#include "linux.h" +#include "measure.h" +#include "memory-util-fundamental.h" +#include "part-discovery.h" +#include "pe.h" +#include "proto/shell-parameters.h" +#include "random-seed.h" +#include "sbat.h" +#include "secure-boot.h" +#include "shim.h" +#include "splash.h" +#include "tpm2-pcr.h" +#include "uki.h" +#include "util.h" +#include "version.h" +#include "vmm.h" + +/* magic string to find in the binary image */ +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-stub " GIT_VERSION " ####"); + +DECLARE_SBAT(SBAT_STUB_SECTION_TEXT); + +static EFI_STATUS combine_initrd( + EFI_PHYSICAL_ADDRESS initrd_base, size_t initrd_size, + const void * const extra_initrds[], const size_t extra_initrd_sizes[], size_t n_extra_initrds, + Pages *ret_initr_pages, size_t *ret_initrd_size) { + + size_t 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 > SIZE_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) { + size_t 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) { + memzero(p, 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 */ + EFI_STUB_FEATURE_RANDOM_SEED | /* We pass a random seed to the kernel */ + EFI_STUB_FEATURE_CMDLINE_ADDONS | /* We pick up .cmdline addons */ + EFI_STUB_FEATURE_CMDLINE_SMBIOS | /* We support extending kernel cmdline from SMBIOS Type #11 */ + EFI_STUB_FEATURE_DEVICETREE_ADDONS | /* We pick up .dtb addons */ + 0; + + assert(loaded_image); + + /* Export the device path this image is started from, if it's not set yet */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); + if (uuid) + efivar_set(MAKE_GUID_PTR(LOADER), u"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(MAKE_GUID_PTR(LOADER), u"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(MAKE_GUID_PTR(LOADER), u"LoaderImageIdentifier", s, 0); + } + + /* if LoaderFirmwareInfo is not set, let's set it */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xasprintf("%ls %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", s, 0); + } + + /* ditto for LoaderFirmwareType */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareType", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xasprintf("UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"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(MAKE_GUID_PTR(LOADER), u"StubInfo", u"systemd-stub " GIT_VERSION, 0); + + (void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"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. + * We also don't allow it if we are in confidential vms and secureboot is on. */ + if (secure_boot_enabled() && (have_cmdline || is_confidential_vm())) + 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, MAKE_GUID_PTR(EFI_SHELL_PARAMETERS_PROTOCOL), (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 = xasprintf("%ls %ls", old, shell->Argv[i]); + } + + mangle_stub_cmdline(*ret); + return true; +} + +static EFI_STATUS load_addons_from_dir( + EFI_FILE *root, + const char16_t *prefix, + char16_t ***items, + size_t *n_items, + size_t *n_allocated) { + + _cleanup_(file_closep) EFI_FILE *extra_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + size_t dirent_size = 0; + EFI_STATUS err; + + assert(root); + assert(prefix); + assert(items); + assert(n_items); + assert(n_allocated); + + err = open_directory(root, prefix, &extra_dir); + if (err == EFI_NOT_FOUND) + /* No extra subdir, that's totally OK */ + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open addons directory '%ls': %m", prefix); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read addons directory of loaded image: %m"); + if (!dirent) /* End of directory */ + break; + + if (dirent->FileName[0] == '.') + continue; + if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (!is_ascii(dirent->FileName)) + continue; + if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */ + continue; + if (!endswith_no_case(dirent->FileName, u".addon.efi")) + continue; + + d = xstrdup16(dirent->FileName); + + if (*n_items + 2 > *n_allocated) { + /* We allocate 16 entries at a time, as a matter of optimization */ + if (*n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + size_t 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 */ + } + + return EFI_SUCCESS; +} + +static void cmdline_append_and_measure_addons( + char16_t *cmdline_global, + char16_t *cmdline_uki, + char16_t **cmdline_append, + bool *ret_parameters_measured) { + + _cleanup_free_ char16_t *tmp = NULL, *merged = NULL; + bool m = false; + + assert(cmdline_append); + assert(ret_parameters_measured); + + if (isempty(cmdline_global) && isempty(cmdline_uki)) + return; + + merged = xasprintf("%ls%ls%ls", + strempty(cmdline_global), + isempty(cmdline_global) || isempty(cmdline_uki) ? u"" : u" ", + strempty(cmdline_uki)); + + mangle_stub_cmdline(merged); + + if (isempty(merged)) + return; + + (void) tpm_log_load_options(merged, &m); + *ret_parameters_measured = m; + + tmp = TAKE_PTR(*cmdline_append); + *cmdline_append = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", merged); +} + +static void dtb_install_addons( + struct devicetree_state *dt_state, + void **dt_bases, + size_t *dt_sizes, + char16_t **dt_filenames, + size_t n_dts, + bool *ret_parameters_measured) { + + int parameters_measured = -1; + EFI_STATUS err; + + assert(dt_state); + assert(n_dts == 0 || (dt_bases && dt_sizes && dt_filenames)); + assert(ret_parameters_measured); + + for (size_t i = 0; i < n_dts; ++i) { + err = devicetree_install_from_memory(dt_state, dt_bases[i], dt_sizes[i]); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading addon devicetree, ignoring: %m"); + else { + bool m = false; + + err = tpm_log_tagged_event( + TPM2_PCR_KERNEL_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(dt_bases[i]), + dt_sizes[i], + DEVICETREE_ADDON_EVENT_TAG_ID, + dt_filenames[i], + &m); + if (err != EFI_SUCCESS) + return (void) log_error_status( + err, + "Unable to add measurement of DTB addon #%zu to PCR %i: %m", + i, + TPM2_PCR_KERNEL_CONFIG); + + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + } + } + + *ret_parameters_measured = parameters_measured; +} + +static void dt_bases_free(void **dt_bases, size_t n_dt) { + assert(dt_bases || n_dt == 0); + + for (size_t i = 0; i < n_dt; ++i) + free(dt_bases[i]); + + free(dt_bases); +} + +static void dt_filenames_free(char16_t **dt_filenames, size_t n_dt) { + assert(dt_filenames || n_dt == 0); + + for (size_t i = 0; i < n_dt; ++i) + free(dt_filenames[i]); + + free(dt_filenames); +} + +static EFI_STATUS load_addons( + EFI_HANDLE stub_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *prefix, + const char *uname, + char16_t **ret_cmdline, + void ***ret_dt_bases, + size_t **ret_dt_sizes, + char16_t ***ret_dt_filenames, + size_t *ret_n_dt) { + + _cleanup_free_ size_t *dt_sizes = NULL; + _cleanup_(strv_freep) char16_t **items = NULL; + _cleanup_(file_closep) EFI_FILE *root = NULL; + _cleanup_free_ char16_t *cmdline = NULL; + size_t n_items = 0, n_allocated = 0, n_dt = 0; + char16_t **dt_filenames = NULL; + void **dt_bases = NULL; + EFI_STATUS err; + + assert(stub_image); + assert(loaded_image); + assert(prefix); + assert(!!ret_dt_bases == !!ret_dt_sizes); + assert(!!ret_dt_bases == !!ret_n_dt); + assert(!!ret_dt_filenames == !!ret_n_dt); + + if (!loaded_image->DeviceHandle) + return EFI_SUCCESS; + + CLEANUP_ARRAY(dt_bases, n_dt, dt_bases_free); + CLEANUP_ARRAY(dt_filenames, n_dt, dt_filenames_free); + + 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. */ + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + err = load_addons_from_dir(root, prefix, &items, &n_items, &n_allocated); + if (err != EFI_SUCCESS) + return err; + + if (n_items == 0) + return EFI_SUCCESS; /* Empty directory */ + + /* 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); + + for (size_t i = 0; i < n_items; i++) { + size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {}; + _cleanup_free_ EFI_DEVICE_PATH *addon_path = NULL; + _cleanup_(unload_imagep) EFI_HANDLE addon = NULL; + EFI_LOADED_IMAGE_PROTOCOL *loaded_addon = NULL; + _cleanup_free_ char16_t *addon_spath = NULL; + + addon_spath = xasprintf("%ls\\%ls", prefix, items[i]); + err = make_file_device_path(loaded_image->DeviceHandle, addon_spath, &addon_path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making device path for %ls: %m", addon_spath); + + /* By using shim_load_image, we cover both the case where the PE files are signed with MoK + * and with DB, and running with or without shim. */ + err = shim_load_image(stub_image, addon_path, &addon); + if (err != EFI_SUCCESS) { + log_error_status(err, + "Failed to read '%ls' from '%ls', ignoring: %m", + items[i], + addon_spath); + continue; + } + + err = BS->HandleProtocol(addon, + MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), + (void **) &loaded_addon); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to find protocol in %ls: %m", items[i]); + + err = pe_memory_locate_sections(loaded_addon->ImageBase, unified_sections, addrs, szs); + if (err != EFI_SUCCESS || + (szs[UNIFIED_SECTION_CMDLINE] == 0 && szs[UNIFIED_SECTION_DTB] == 0)) { + if (err == EFI_SUCCESS) + err = EFI_NOT_FOUND; + log_error_status(err, + "Unable to locate embedded .cmdline/.dtb sections in %ls, ignoring: %m", + items[i]); + continue; + } + + /* We want to enforce that addons are not UKIs, i.e.: they must not embed a kernel. */ + if (szs[UNIFIED_SECTION_LINUX] > 0) { + log_error_status(EFI_INVALID_PARAMETER, "%ls is a UKI, not an addon, ignoring: %m", items[i]); + continue; + } + + /* Also enforce that, in case it is specified, .uname matches as a quick way to allow + * enforcing compatibility with a specific UKI only */ + if (uname && szs[UNIFIED_SECTION_UNAME] > 0 && + !strneq8(uname, + (char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_UNAME], + szs[UNIFIED_SECTION_UNAME])) { + log_error(".uname mismatch between %ls and UKI, ignoring", items[i]); + continue; + } + + if (ret_cmdline && szs[UNIFIED_SECTION_CMDLINE] > 0) { + _cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), + *extra16 = xstrn8_to_16((char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_CMDLINE], + szs[UNIFIED_SECTION_CMDLINE]); + cmdline = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", extra16); + } + + if (ret_dt_bases && szs[UNIFIED_SECTION_DTB] > 0) { + dt_sizes = xrealloc(dt_sizes, + n_dt * sizeof(size_t), + (n_dt + 1) * sizeof(size_t)); + dt_sizes[n_dt] = szs[UNIFIED_SECTION_DTB]; + + dt_bases = xrealloc(dt_bases, + n_dt * sizeof(void *), + (n_dt + 1) * sizeof(void *)); + dt_bases[n_dt] = xmemdup((uint8_t*)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_DTB], + dt_sizes[n_dt]); + + dt_filenames = xrealloc(dt_filenames, + n_dt * sizeof(char16_t *), + (n_dt + 1) * sizeof(char16_t *)); + dt_filenames[n_dt] = xstrdup16(items[i]); + + ++n_dt; + } + } + + if (ret_cmdline && !isempty(cmdline)) + *ret_cmdline = TAKE_PTR(cmdline); + + if (ret_n_dt && n_dt > 0) { + *ret_dt_filenames = TAKE_PTR(dt_filenames); + *ret_dt_bases = TAKE_PTR(dt_bases); + *ret_dt_sizes = TAKE_PTR(dt_sizes); + *ret_n_dt = n_dt; + } + + return EFI_SUCCESS; +} + +static EFI_STATUS run(EFI_HANDLE image) { + _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; + void **dt_bases_addons_global = NULL, **dt_bases_addons_uki = NULL; + char16_t **dt_filenames_addons_global = NULL, **dt_filenames_addons_uki = NULL; + _cleanup_free_ size_t *dt_sizes_addons_global = NULL, *dt_sizes_addons_uki = NULL; + size_t linux_size, initrd_size, dt_size, n_dts_addons_global = 0, n_dts_addons_uki = 0; + 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, *cmdline_addons_global = NULL, *cmdline_addons_uki = NULL; + int sections_measured = -1, parameters_measured = -1; + _cleanup_free_ char *uname = NULL; + bool sysext_measured = false, m; + uint64_t loader_features = 0; + EFI_STATUS err; + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); + + if (loaded_image->DeviceHandle && /* Handle case, where bootloader doesn't support DeviceHandle. */ + (efivar_get_uint64_le(MAKE_GUID_PTR(LOADER), u"LoaderFeatures", &loader_features) != EFI_SUCCESS || + !FLAGS_SET(loader_features, EFI_LOADER_FEATURE_RANDOM_SEED))) { + _cleanup_(file_closep) EFI_FILE *esp_dir = NULL; + + err = partition_open(MAKE_GUID_PTR(ESP), loaded_image->DeviceHandle, NULL, &esp_dir); + if (err == EFI_SUCCESS) /* Non-fatal on failure, so that we still boot without it. */ + (void) process_random_seed(esp_dir); + } + + 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(err, "Unable to locate embedded .linux section: %m"); + } + + CLEANUP_ARRAY(dt_bases_addons_global, n_dts_addons_global, dt_bases_free); + CLEANUP_ARRAY(dt_bases_addons_uki, n_dts_addons_uki, dt_bases_free); + CLEANUP_ARRAY(dt_filenames_addons_global, n_dts_addons_global, dt_filenames_free); + CLEANUP_ARRAY(dt_filenames_addons_uki, n_dts_addons_uki, dt_filenames_free); + + /* Now that we have the UKI sections loaded, also load global first and then local (per-UKI) + * addons. The data is loaded at once, and then used later. */ + err = load_addons( + image, + loaded_image, + u"\\loader\\addons", + uname, + &cmdline_addons_global, + &dt_bases_addons_global, + &dt_sizes_addons_global, + &dt_filenames_addons_global, + &n_dts_addons_global); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading global addons, ignoring: %m"); + + /* Some bootloaders always pass NULL in FilePath, so we need to check for it here. */ + _cleanup_free_ char16_t *dropin_dir = get_extra_dir(loaded_image->FilePath); + if (dropin_dir) { + err = load_addons( + image, + loaded_image, + dropin_dir, + uname, + &cmdline_addons_uki, + &dt_bases_addons_uki, + &dt_sizes_addons_uki, + &dt_filenames_addons_uki, + &n_dts_addons_uki); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading UKI-specific addons, ignoring: %m"); + } + + /* 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( + TPM2_PCR_KERNEL_BOOT, + 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( + TPM2_PCR_KERNEL_BOOT, + 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(MAKE_GUID_PTR(LOADER), u"StubPcrKernelImage", TPM2_PCR_KERNEL_BOOT, 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 (szs[UNIFIED_SECTION_UNAME] > 0) + uname = xstrndup8((char *)loaded_image->ImageBase + addrs[UNIFIED_SECTION_UNAME], + szs[UNIFIED_SECTION_UNAME]); + + 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); + } + + /* If we have any extra command line to add via PE addons, load them now and append, and + * measure the additions together, after the embedded options, but before the smbios ones, + * so that the order is reversed from "most hardcoded" to "most dynamic". The global addons are + * loaded first, and the image-specific ones later, for the same reason. */ + cmdline_append_and_measure_addons(cmdline_addons_global, cmdline_addons_uki, &cmdline, &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + /* SMBIOS OEM Strings data is controlled by the host admin and not covered + * by the VM attestation, so MUST NOT be trusted when in a confidential VM */ + if (!is_confidential_vm()) { + const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra"); + if (extra) { + _cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), *extra16 = xstr8_to_16(extra); + cmdline = xasprintf("%ls %ls", tmp, extra16); + + /* SMBIOS strings are measured in PCR1, but we also want to measure them in our specific + * PCR12, as firmware-owned PCRs are very difficult to use as they'll contain unpredictable + * measurements that are not under control of the machine owner. */ + m = false; + (void) tpm_log_load_options(extra16, &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + } + } + + export_variables(loaded_image); + + if (pack_cpio(loaded_image, + NULL, + u".cred", + ".extra/credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG, + u"Credentials initrd", + &credential_initrd, + &credential_initrd_size, + &m) == EFI_SUCCESS) + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (pack_cpio(loaded_image, + u"\\loader\\credentials", + u".cred", + ".extra/global_credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG, + u"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, + u".raw", + ".extra/sysext", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ TPM2_PCR_SYSEXTS, + u"System extension initrd", + &sysext_initrd, + &sysext_initrd_size, + &m) == EFI_SUCCESS) + sysext_measured = m; + + dt_size = szs[UNIFIED_SECTION_DTB]; + dt_base = dt_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_DTB] : 0; + + /* First load the base device tree, then fix it up using addons - global first, then per-UKI. */ + 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_status(err, "Error loading embedded devicetree: %m"); + } + + dtb_install_addons(&dt_state, + dt_bases_addons_global, + dt_sizes_addons_global, + dt_filenames_addons_global, + n_dts_addons_global, + &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + dtb_install_addons(&dt_state, + dt_bases_addons_uki, + dt_sizes_addons_uki, + dt_filenames_addons_uki, + n_dts_addons_uki, + &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (parameters_measured > 0) + (void) efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"StubPcrKernelParameters", TPM2_PCR_KERNEL_CONFIG, 0); + if (sysext_measured) + (void) efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"StubPcrInitRDSysExts", TPM2_PCR_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", + u"tpm2-pcr-signature.json", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ UINT32_MAX, + /* 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", + u"tpm2-pcr-public-key.pem", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ UINT32_MAX, + /* 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; + + _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); + } + + 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; +} + +DEFINE_EFI_MAIN_FUNCTION(run, "systemd-stub", /*wait_for_debugger=*/false); diff --git a/src/boot/efi/test-bcd.c b/src/boot/efi/test-bcd.c new file mode 100644 index 0000000..3f93ca0 --- /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, SIZE_MAX); + 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..b71a0c3 --- /dev/null +++ b/src/boot/efi/test-efi-string.c @@ -0,0 +1,794 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fnmatch.h> + +#include "efi-string.h" +#include "fileio.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]); + + assert_se(strchr8(str, 0) == str + strlen8(str)); +} + +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]); + + assert_se(strchr16(str, 0) == str + strlen16(str)); +} + +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); +} + +TEST(startswith8) { + assert_se(streq8(startswith8("", ""), "")); + assert_se(streq8(startswith8("x", ""), "x")); + assert_se(!startswith8("", "x")); + assert_se(!startswith8("", "xxxxxxxx")); + assert_se(streq8(startswith8("xxx", "x"), "xx")); + assert_se(streq8(startswith8("xxx", "xx"), "x")); + assert_se(streq8(startswith8("xxx", "xxx"), "")); + assert_se(!startswith8("xxx", "xxxx")); + assert_se(!startswith8(NULL, "")); +} + +#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(parse_boolean) { + bool b; + + assert_se(!parse_boolean(NULL, &b)); + assert_se(!parse_boolean("", &b)); + assert_se(!parse_boolean("ja", &b)); + assert_se(parse_boolean("1", &b) && b == true); + assert_se(parse_boolean("y", &b) && b == true); + assert_se(parse_boolean("yes", &b) && b == true); + assert_se(parse_boolean("t", &b) && b == true); + assert_se(parse_boolean("true", &b) && b == true); + assert_se(parse_boolean("on", &b) && b == true); + assert_se(parse_boolean("0", &b) && b == false); + assert_se(parse_boolean("n", &b) && b == false); + assert_se(parse_boolean("no", &b) && b == false); + assert_se(parse_boolean("f", &b) && b == false); + assert_se(parse_boolean("false", &b) && b == false); + assert_se(parse_boolean("off", &b) && b == false); +} + +TEST(line_get_key_value) { + char s1[] = "key=value\n" + " \t # comment line \n" + "k-e-y=\"quoted value\"\n\r" + " wrong= 'quotes' \n" + "odd= stripping # with comments "; + char s2[] = "this parser\n" + "\t\t\t# is\t\r" + " also\tused \r\n" + "for \"the conf\"\n" + "format\t !!"; + size_t pos = 0; + char *key, *value; + + assert_se(!line_get_key_value((char[]){ "" }, "=", &pos, &key, &value)); + assert_se(!line_get_key_value((char[]){ "\t" }, " \t", &pos, &key, &value)); + + pos = 0; + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "key")); + assert_se(streq8(value, "value")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "k-e-y")); + assert_se(streq8(value, "quoted value")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "wrong")); + assert_se(streq8(value, " 'quotes'")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "odd")); + assert_se(streq8(value, " stripping # with comments")); + assert_se(!line_get_key_value(s1, "=", &pos, &key, &value)); + + pos = 0; + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "this")); + assert_se(streq8(value, "parser")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "also")); + assert_se(streq8(value, "used")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "for")); + assert_se(streq8(value, "the conf")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "format")); + assert_se(streq8(value, "!!")); + assert_se(!line_get_key_value(s2, " \t", &pos, &key, &value)); + + /* Let's make sure we don't fail on real os-release data. */ + _cleanup_free_ char *osrel = NULL; + if (read_full_file("/usr/lib/os-release", &osrel, NULL) >= 0) { + pos = 0; + while (line_get_key_value(osrel, "=", &pos, &key, &value)) { + assert_se(key); + assert_se(value); + printf("%s = %s\n", key, value); + } + } +} + +TEST(hexdump) { + char16_t *hex; + + hex = hexdump(NULL, 0); + assert(streq16(hex, u"")); + free(hex); + + hex = hexdump("1", 2); + assert(streq16(hex, u"3100")); + free(hex); + + hex = hexdump("abc", 4); + assert(streq16(hex, u"61626300")); + free(hex); + + hex = hexdump((uint8_t[]){ 0x0, 0x42, 0xFF, 0xF1, 0x1F }, 5); + assert(streq16(hex, u"0042fff11f")); + free(hex); +} + +_printf_(1, 2) static void test_printf_one(const char *format, ...) { + va_list ap, ap_efi; + va_start(ap, format); + va_copy(ap_efi, ap); + + _cleanup_free_ char *buf = NULL; + int r = vasprintf(&buf, format, ap); + assert_se(r >= 0); + log_info("/* %s(%s) -> \"%.100s\" */", __func__, format, buf); + + _cleanup_free_ char16_t *buf_efi = xvasprintf_status(0, format, ap_efi); + + bool eq = true; + for (size_t i = 0; i <= (size_t) r; i++) { + if (buf[i] != buf_efi[i]) + eq = false; + buf[i] = buf_efi[i]; + } + + log_info("%.100s", buf); + assert_se(eq); + + va_end(ap); + va_end(ap_efi); +} + +TEST(xvasprintf_status) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-zero-length" + test_printf_one(""); +#pragma GCC diagnostic pop + test_printf_one("string"); + test_printf_one("%%-%%%%"); + + test_printf_one("%p %p %32p %*p %*p", NULL, (void *) 0xF, &errno, 0, &saved_argc, 20, &saved_argv); + test_printf_one("%-10p %-32p %-*p %-*p", NULL, &errno, 0, &saved_argc, 20, &saved_argv); + + test_printf_one("%c %3c %*c %*c %-8c", '1', '!', 0, 'a', 9, '_', '>'); + + test_printf_one("%s %s %s", "012345", "6789", "ab"); + test_printf_one("%.4s %.4s %.4s %.0s", "cdefgh", "ijkl", "mn", "@"); + test_printf_one("%8s %8s %8s", "opqrst", "uvwx", "yz"); + test_printf_one("%8.4s %8.4s %8.4s %8.0s", "ABCDEF", "GHIJ", "KL", "$"); + test_printf_one("%4.8s %4.8s %4.8s", "ABCDEFGHIJ", "ABCDEFGH", "ABCD"); + + test_printf_one("%.*s %.*s %.*s %.*s", 4, "012345", 4, "6789", 4, "ab", 0, "&"); + test_printf_one("%*s %*s %*s", 8, "cdefgh", 8, "ijkl", 8, "mn"); + test_printf_one("%*.*s %*.*s %*.*s %*.*s", 8, 4, "opqrst", 8, 4, "uvwx", 8, 4, "yz", 8, 0, "#"); + test_printf_one("%*.*s %*.*s %*.*s", 3, 8, "OPQRST", 3, 8, "UVWX", 3, 8, "YZ"); + + test_printf_one("%ls %ls %ls", L"012345", L"6789", L"ab"); + test_printf_one("%.4ls %.4ls %.4ls %.0ls", L"cdefgh", L"ijkl", L"mn", L"@"); + test_printf_one("%8ls %8ls %8ls", L"opqrst", L"uvwx", L"yz"); + test_printf_one("%8.4ls %8.4ls %8.4ls %8.0ls", L"ABCDEF", L"GHIJ", L"KL", L"$"); + test_printf_one("%4.8ls %4.8ls %4.8ls", L"ABCDEFGHIJ", L"ABCDEFGH", L"ABCD"); + + test_printf_one("%.*ls %.*ls %.*ls %.*ls", 4, L"012345", 4, L"6789", 4, L"ab", 0, L"&"); + test_printf_one("%*ls %*ls %*ls", 8, L"cdefgh", 8, L"ijkl", 8, L"mn"); + test_printf_one("%*.*ls %*.*ls %*.*ls %*.*ls", 8, 4, L"opqrst", 8, 4, L"uvwx", 8, 4, L"yz", 8, 0, L"#"); + test_printf_one("%*.*ls %*.*ls %*.*ls", 3, 8, L"OPQRST", 3, 8, L"UVWX", 3, 8, L"YZ"); + + test_printf_one("%-14s %-10.0s %-10.3s", "left", "", "chopped"); + test_printf_one("%-14ls %-10.0ls %-10.3ls", L"left", L"", L"chopped"); + + test_printf_one("%.6s", (char[]){ 'n', 'o', ' ', 'n', 'u', 'l' }); + test_printf_one("%.6ls", (wchar_t[]){ 'n', 'o', ' ', 'n', 'u', 'l' }); + + test_printf_one("%u %u %u", 0U, 42U, 1234567890U); + test_printf_one("%i %i %i", 0, -42, -1234567890); + test_printf_one("%x %x %x", 0x0U, 0x42U, 0x123ABCU); + test_printf_one("%X %X %X", 0x1U, 0x43U, 0x234BCDU); + test_printf_one("%#x %#x %#x", 0x2U, 0x44U, 0x345CDEU); + test_printf_one("%#X %#X %#X", 0x3U, 0x45U, 0x456FEDU); + + test_printf_one("%u %lu %llu %zu", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%i %i %zi", INT_MIN, INT_MAX, SSIZE_MAX); + test_printf_one("%li %li %lli %lli", LONG_MIN, LONG_MAX, LLONG_MIN, LLONG_MAX); + test_printf_one("%x %#lx %llx %#zx", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%X %#lX %llX %#zX", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%ju %ji %ji", UINTMAX_MAX, INTMAX_MIN, INTMAX_MAX); + test_printf_one("%ti %ti", PTRDIFF_MIN, PTRDIFF_MAX); + + test_printf_one("%" PRIu32 " %" PRIi32 " %" PRIi32, UINT32_MAX, INT32_MIN, INT32_MAX); + test_printf_one("%" PRIx32 " %" PRIX32, UINT32_MAX, UINT32_MAX); + test_printf_one("%#" PRIx32 " %#" PRIX32, UINT32_MAX, UINT32_MAX); + + test_printf_one("%" PRIu64 " %" PRIi64 " %" PRIi64, UINT64_MAX, INT64_MIN, INT64_MAX); + test_printf_one("%" PRIx64 " %" PRIX64, UINT64_MAX, UINT64_MAX); + test_printf_one("%#" PRIx64 " %#" PRIX64, UINT64_MAX, UINT64_MAX); + + test_printf_one("%.11u %.11i %.11x %.11X %#.11x %#.11X", 1U, -2, 3U, 0xA1U, 0xB2U, 0xC3U); + test_printf_one("%13u %13i %13x %13X %#13x %#13X", 4U, -5, 6U, 0xD4U, 0xE5U, 0xF6U); + test_printf_one("%9.5u %9.5i %9.5x %9.5X %#9.5x %#9.5X", 7U, -8, 9U, 0xA9U, 0xB8U, 0xC7U); + test_printf_one("%09u %09i %09x %09X %#09x %#09X", 4U, -5, 6U, 0xD6U, 0xE5U, 0xF4U); + + test_printf_one("%*u %.*u %*i %.*i", 15, 42U, 15, 43U, 15, -42, 15, -43); + test_printf_one("%*.*u %*.*i", 14, 10, 13U, 14, 10, -14); + test_printf_one("%*x %*X %.*x %.*X", 15, 0x1AU, 15, 0x2BU, 15, 0x3CU, 15, 0x4DU); + test_printf_one("%#*x %#*X %#.*x %#.*X", 15, 0xA1U, 15, 0xB2U, 15, 0xC3U, 15, 0xD4U); + test_printf_one("%*.*x %*.*X", 14, 10, 0x1AU, 14, 10, 0x2BU); + test_printf_one("%#*.*x %#*.*X", 14, 10, 0x3CU, 14, 10, 0x4DU); + + test_printf_one("%+.5i %+.5i % .7i % .7i", -15, 51, -15, 51); + test_printf_one("%+5.i %+5.i % 7.i % 7.i", -15, 51, -15, 51); + + test_printf_one("%-10u %-10i %-10x %#-10X %- 10i", 1u, -2, 0xA2D2u, 0XB3F4u, -512); + test_printf_one("%-10.6u %-10.6i %-10.6x %#-10.6X %- 10.6i", 1u, -2, 0xA2D2u, 0XB3F4u, -512); + test_printf_one("%-6.10u %-6.10i %-6.10x %#-6.10X %- 6.10i", 3u, -4, 0x2A2Du, 0X3B4Fu, -215); + test_printf_one("%*.u %.*i %.*i", -4, 9u, -4, 8, -4, -6); + + test_printf_one("%.0u %.0i %.0x %.0X", 0u, 0, 0u, 0u); + test_printf_one("%.*u %.*i %.*x %.*X", 0, 0u, 0, 0, 0, 0u, 0, 0u); + test_printf_one("%*u %*i %*x %*X", -1, 0u, -1, 0, -1, 0u, -1, 0u); + + test_printf_one("%*s%*s%*s", 256, "", 256, "", 4096, ""); /* Test buf growing. */ + test_printf_one("%0*i%0*i%0*i", 256, 0, 256, 0, 4096, 0); /* Test buf growing. */ + test_printf_one("%0*i", INT16_MAX, 0); /* Poor programmer's memzero. */ + + /* Non printf-compatible behavior tests below. */ + char16_t *s; + + assert_se(s = xasprintf_status(0, "\n \r \r\n")); + assert_se(streq16(s, u"\r\n \r \r\r\n")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_SUCCESS, "%m")); + assert_se(streq16(s, u"Success")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_SUCCESS, "%15m")); + assert_se(streq16(s, u" Success")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_LOAD_ERROR, "%m")); + assert_se(streq16(s, u"Load error")); + s = mfree(s); + + assert_se(s = xasprintf_status(0x42, "%m")); + assert_se(streq16(s, u"0x42")); + s = mfree(s); +} + +TEST(efi_memchr) { + assert_se(streq8(efi_memchr("abcde", 'c', 5), "cde")); + assert_se(streq8(efi_memchr("abcde", 'c', 3), "cde")); + assert_se(streq8(efi_memchr("abcde", 'c', 2), NULL)); + assert_se(streq8(efi_memchr("abcde", 'c', 7), "cde")); + assert_se(streq8(efi_memchr("abcde", 'q', 5), NULL)); + assert_se(streq8(efi_memchr("abcde", 'q', 0), NULL)); + /* Test that the character is interpreted as unsigned char. */ + assert_se(streq8(efi_memchr("abcde", 'a', 6), efi_memchr("abcde", 'a' + 0x100, 6))); + assert_se(streq8(efi_memchr("abcde", 0, 6), "")); + assert_se(efi_memchr(NULL, 0, 0) == NULL); +} + +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..873b9fe --- /dev/null +++ b/src/boot/efi/ticks.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ticks.h" +#include "util.h" +#include "vmm.h" + +#if defined(__i386__) || defined(__x86_64__) +# include <cpuid.h> + +static uint64_t ticks_read_arch(void) { + /* 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; + + return __builtin_ia32_rdtsc(); +} + +static uint64_t ticks_freq_arch(void) { + /* Detect TSC frequency from CPUID information if available. */ + + unsigned max_leaf, ebx, ecx, edx; + if (__get_cpuid(0, &max_leaf, &ebx, &ecx, &edx) == 0) + return 0; + + /* Leaf 0x15 is Intel only. */ + if (max_leaf < 0x15 || ebx != signature_INTEL_ebx || ecx != signature_INTEL_ecx || + edx != signature_INTEL_edx) + return 0; + + unsigned denominator, numerator, crystal_hz; + __cpuid(0x15, denominator, numerator, crystal_hz, edx); + if (denominator == 0 || numerator == 0) + return 0; + + uint64_t freq = crystal_hz; + if (crystal_hz == 0) { + /* If the crystal frequency is not available, try to deduce it from + * the processor frequency leaf if available. */ + if (max_leaf < 0x16) + return 0; + + unsigned core_mhz; + __cpuid(0x16, core_mhz, ebx, ecx, edx); + freq = core_mhz * 1000ULL * 1000ULL * denominator / numerator; + } + + return freq * numerator / denominator; +} + +#elif defined(__aarch64__) + +static uint64_t ticks_read_arch(void) { + uint64_t val; + asm volatile("mrs %0, cntvct_el0" : "=r"(val)); + return val; +} + +static uint64_t ticks_freq_arch(void) { + uint64_t freq; + asm volatile("mrs %0, cntfrq_el0" : "=r"(freq)); + return freq; +} + +#else + +static uint64_t ticks_read_arch(void) { + return 0; +} + +static uint64_t ticks_freq_arch(void) { + return 0; +} + +#endif + +static uint64_t ticks_freq(void) { + static uint64_t cache = 0; + + if (cache != 0) + return cache; + + cache = ticks_freq_arch(); + if (cache != 0) + return cache; + + /* As a fallback, count ticks during a millisecond delay. */ + uint64_t ticks_start = ticks_read_arch(); + BS->Stall(1000); + uint64_t ticks_end = ticks_read_arch(); + + if (ticks_end < ticks_start) /* Check for an overflow (which is not that unlikely, given on some + * archs the value is 32-bit) */ + return 0; + + cache = (ticks_end - ticks_start) * 1000UL; + return cache; +} + +uint64_t time_usec(void) { + uint64_t ticks = ticks_read_arch(); + if (ticks == 0) + return 0; + + uint64_t 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/ubsan.c b/src/boot/efi/ubsan.c new file mode 100644 index 0000000..9512046 --- /dev/null +++ b/src/boot/efi/ubsan.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "log.h" + +typedef struct { + const char *filename; + uint32_t line; + uint32_t column; +} SourceLocation; + +/* Note that all ubsan handlers have a pointer to a type-specific struct passed as first argument. + * Since we do not inspect the extra data in it we can just treat it as a SourceLocation struct + * directly to keep things simple. */ + +#define HANDLER(name, ...) \ + _used_ _noreturn_ void __ubsan_handle_##name(__VA_ARGS__); \ + void __ubsan_handle_##name(__VA_ARGS__) { \ + log_error("systemd-boot: %s in %s@%u:%u", \ + __func__, \ + location->filename, \ + location->line, \ + location->column); \ + freeze(); \ + } + +#define UNARY_HANDLER(name) HANDLER(name, SourceLocation *location, uintptr_t v) +#define BINARY_HANDLER(name) HANDLER(name, SourceLocation *location, uintptr_t v1, uintptr_t v2) + +UNARY_HANDLER(load_invalid_value); +UNARY_HANDLER(negate_overflow); +UNARY_HANDLER(out_of_bounds); +UNARY_HANDLER(type_mismatch_v1); +UNARY_HANDLER(vla_bound_not_positive); + +BINARY_HANDLER(add_overflow); +BINARY_HANDLER(divrem_overflow); +BINARY_HANDLER(implicit_conversion); +BINARY_HANDLER(mul_overflow); +BINARY_HANDLER(pointer_overflow); +BINARY_HANDLER(shift_out_of_bounds); +BINARY_HANDLER(sub_overflow); + +HANDLER(builtin_unreachable, SourceLocation *location); +HANDLER(invalid_builtin, SourceLocation *location); +HANDLER(nonnull_arg, SourceLocation *location); +HANDLER(nonnull_return_v1, SourceLocation *attr_location, SourceLocation *location); diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c new file mode 100644 index 0000000..e56ccfd --- /dev/null +++ b/src/boot/efi/util.c @@ -0,0 +1,705 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "memory-util-fundamental.h" +#include "proto/device-path.h" +#include "proto/simple-text-io.h" +#include "ticks.h" +#include "util.h" +#include "version.h" + +EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, size_t 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, size_t i, uint32_t flags) { + assert(vendor); + assert(name); + + _cleanup_free_ char16_t *str = xasprintf("%zu", 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_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags) { + EFI_STATUS err; + + assert(vendor); + assert(name); + + /* We could be wiping a non-volatile variable here and the spec makes no guarantees that won't incur + * in an extra write (and thus wear out). So check and clear only if needed. */ + err = efivar_get_raw(vendor, name, NULL, NULL); + if (err == EFI_SUCCESS) + return efivar_set_raw(vendor, name, NULL, 0, flags); + + return err; +} + +EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **ret) { + _cleanup_free_ char16_t *buf = NULL; + EFI_STATUS err; + char16_t *val; + size_t 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 (!ret) + 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) { + *ret = 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 */ + + *ret = val; + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t *ret) { + _cleanup_free_ char16_t *val = NULL; + EFI_STATUS err; + uint64_t u; + + assert(vendor); + assert(name); + + err = efivar_get(vendor, name, &val); + if (err != EFI_SUCCESS) + return err; + + if (!parse_number16(val, &u, NULL) || u > SIZE_MAX) + return EFI_INVALID_PARAMETER; + + if (ret) + *ret = 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; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err != EFI_SUCCESS) + return err; + + if (size != sizeof(uint32_t)) + return EFI_BUFFER_TOO_SMALL; + + if (ret) + *ret = (uint32_t) buf[0] << 0U | (uint32_t) buf[1] << 8U | (uint32_t) buf[2] << 16U | + (uint32_t) buf[3] << 24U; + + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) { + _cleanup_free_ char *buf = NULL; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err != EFI_SUCCESS) + return err; + + if (size != sizeof(uint64_t)) + return EFI_BUFFER_TOO_SMALL; + + if (ret) + *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 EFI_SUCCESS; +} + +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **ret, size_t *ret_size) { + EFI_STATUS err; + + assert(vendor); + assert(name); + + size_t size = 0; + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, NULL); + if (err != EFI_BUFFER_TOO_SMALL) + return err; + + _cleanup_free_ void *buf = xmalloc(size); + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, buf); + if (err != EFI_SUCCESS) + return err; + + if (ret) + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; + + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret) { + _cleanup_free_ char *b = NULL; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &b, &size); + if (err != EFI_SUCCESS) + return err; + + if (ret) + *ret = *b > 0; + + return EFI_SUCCESS; +} + +void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec) { + assert(vendor); + assert(name); + + if (usec == 0) + usec = time_usec(); + if (usec == 0) + return; + + _cleanup_free_ char16_t *str = xasprintf("%" PRIu64, 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; +} + +static bool shall_be_whitespace(char16_t c) { + return c <= 0x20U || c == 0x7FU; /* All control characters + space */ +} + +char16_t* mangle_stub_cmdline(char16_t *cmdline) { + char16_t *p, *q, *e; + + if (!cmdline) + return cmdline; + + p = q = cmdline; + + /* Skip initial whitespace */ + while (shall_be_whitespace(*p)) + p++; + + /* Turn inner control characters into proper spaces */ + for (e = p; *p != 0; p++) { + if (shall_be_whitespace(*p)) { + *(q++) = ' '; + continue; + } + + *(q++) = *p; + e = q; /* remember last non-whitespace char */ + } + + /* Chop off trailing whitespace */ + *e = 0; + return cmdline; +} + +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, size_t off, size_t size, char **ret, size_t *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(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. */ + size_t 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. */ + memzero(buf + size, extra); + + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; + + return err; +} + +void print_at(size_t x, size_t y, size_t 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(size_t attr) { + log_wait(); + ST->ConOut->SetAttribute(ST->ConOut, attr); + ST->ConOut->ClearScreen(ST->ConOut); +} + +void sort_pointer_array( + void **array, + size_t n_members, + compare_pointer_func_t compare) { + + assert(array || n_members == 0); + assert(compare); + + if (n_members <= 1) + return; + + for (size_t i = 1; i < n_members; i++) { + size_t 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(EFI_FILE *handle, EFI_FILE_INFO **ret, size_t *ret_size) { + size_t size = EFI_FILE_INFO_MIN_SIZE; + _cleanup_free_ EFI_FILE_INFO *fi = NULL; + EFI_STATUS err; + + assert(handle); + assert(ret); + + fi = xmalloc(size); + err = handle->GetInfo(handle, MAKE_GUID_PTR(EFI_FILE_INFO), &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, MAKE_GUID_PTR(EFI_FILE_INFO), &size, fi); + } + + if (err != EFI_SUCCESS) + return err; + + *ret = TAKE_PTR(fi); + + if (ret_size) + *ret_size = size; + + return EFI_SUCCESS; +} + +EFI_STATUS readdir( + EFI_FILE *handle, + EFI_FILE_INFO **buffer, + size_t *buffer_size) { + + EFI_STATUS err; + size_t 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) { + sz = EFI_FILE_INFO_MIN_SIZE; + *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(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(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndicationsSupported", &osind); + if (err != EFI_SUCCESS) + return 0; + + return osind; +} + +__attribute__((noinline)) void notify_debugger(const char *identity, volatile bool wait) { +#ifdef EFI_DEBUG + printf("%s@%p %s\n", identity, __executable_start, GIT_VERSION); + if (wait) + printf("Waiting for debugger to attach...\n"); + + /* 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. */ + 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 +} + +#if defined(__i386__) || defined(__x86_64__) +static uint8_t inb(uint16_t port) { + uint8_t value; + asm volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static void outb(uint16_t port, uint8_t value) { + asm volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +void beep(unsigned 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, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), (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; +} + +void *find_configuration_table(const EFI_GUID *guid) { + for (size_t i = 0; i < ST->NumberOfTableEntries; i++) + if (efi_guid_equal(&ST->ConfigurationTable[i].VendorGuid, guid)) + return ST->ConfigurationTable[i].VendorTable; + + return NULL; +} + +static void remove_boot_count(char16_t *path) { + char16_t *prefix_end; + const char16_t *tail; + uint64_t ignored; + + assert(path); + + prefix_end = strchr16(path, '+'); + if (!prefix_end) + return; + + tail = prefix_end + 1; + + if (!parse_number16(tail, &ignored, &tail)) + return; + + if (*tail == '-') { + ++tail; + if (!parse_number16(tail, &ignored, &tail)) + return; + } + + if (!IN_SET(*tail, '\0', '.')) + return; + + strcpy16(prefix_end, tail); +} + +char16_t *get_extra_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; !device_path_is_end(node); + node = device_path_next_node(node)) + if (node->Type != MEDIA_DEVICE_PATH || node->SubType != 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); + remove_boot_count(file_path_str); + return xasprintf("%ls.extra.d", file_path_str); +} + +void *xmalloc(size_t size) { + void *p = NULL; + assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS); + return p; +} diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h new file mode 100644 index 0000000..0306e32 --- /dev/null +++ b/src/boot/efi/util.h @@ -0,0 +1,213 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "log.h" +#include "proto/file-io.h" +#include "string-util-fundamental.h" + +/* This is provided by the linker. */ +extern uint8_t __executable_start[]; + +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_ +void *xmalloc(size_t size); + +_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 *t = xmalloc(new_size); + new_size = MIN(old_size, new_size); + if (new_size > 0) + memcpy(t, p, new_size); + free(p); + return t; +} + +_malloc_ _alloc_(2) _returns_nonnull_ _warn_unused_result_ +static inline void* xmemdup(const void *p, size_t l) { + return memcpy(xmalloc(l), p, l); +} + +#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 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, size_t size, uint32_t flags); +EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t 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_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags); + +EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **ret); +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **ret, size_t *ret_size); +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t *ret); +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); +char16_t *mangle_stub_cmdline(char16_t *cmdline); + +EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf); +EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, size_t off, size_t size, char **content, size_t *content_size); + +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 \ + { 0x4a67b082, 0x0a4c, 0x41cf, { 0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f } } + +/* Note that GUID is evaluated multiple times! */ +#define GUID_FORMAT_STR "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X" +#define GUID_FORMAT_VAL(g) (g).Data1, (g).Data2, (g).Data3, (g).Data4[0], (g).Data4[1], \ + (g).Data4[2], (g).Data4[3], (g).Data4[4], (g).Data4[5], (g).Data4[6], (g).Data4[7] + +void print_at(size_t x, size_t y, size_t attr, const char16_t *str); +void clear_screen(size_t attr); + +typedef int (*compare_pointer_func_t)(const void *a, const void *b); +void sort_pointer_array(void **array, size_t n_members, compare_pointer_func_t compare); + +EFI_STATUS get_file_info(EFI_FILE *handle, EFI_FILE_INFO **ret, size_t *ret_size); +EFI_STATUS readdir(EFI_FILE *handle, EFI_FILE_INFO **buffer, size_t *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 64-bit, even on + * 32-bit 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 32-bit systems the address might not be convertible (as pointers are 32-bit but + * EFI_PHYSICAL_ADDRESS 64-bit) */ + assert(addr <= UINTPTR_MAX); + return (void *) (uintptr_t) addr; +} + +uint64_t get_os_indications_supported(void); + +/* If EFI_DEBUG, print our name and version and also report the address of the image base so a debugger can + * be attached. See debug-sd-boot.sh for how this can be done. */ +void notify_debugger(const char *identity, bool wait); + +/* On x86 the compiler assumes a different incoming stack alignment than what we get. + * This will cause long long variables to be misaligned when building with + * '-mlong-double' (for correct struct layouts). Normally, the compiler realigns the + * stack itself on entry, but we have to do this ourselves here as the compiler does + * not know that this is our entry point. */ +#ifdef __i386__ +# define _realign_stack_ __attribute__((force_align_arg_pointer)) +#else +# define _realign_stack_ +#endif + +#define DEFINE_EFI_MAIN_FUNCTION(func, identity, wait_for_debugger) \ + EFI_SYSTEM_TABLE *ST; \ + EFI_BOOT_SERVICES *BS; \ + EFI_RUNTIME_SERVICES *RT; \ + _realign_stack_ \ + EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table); \ + EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { \ + ST = system_table; \ + BS = system_table->BootServices; \ + RT = system_table->RuntimeServices; \ + __stack_chk_guard_init(); \ + notify_debugger((identity), (wait_for_debugger)); \ + EFI_STATUS err = func(image); \ + log_wait(); \ + return err; \ + } + +#if defined(__i386__) || defined(__x86_64__) +void beep(unsigned beep_count); +#else +static inline void beep(unsigned beep_count) {} +#endif + +EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file); + +static inline bool efi_guid_equal(const EFI_GUID *a, const EFI_GUID *b) { + return memcmp(a, b, sizeof(EFI_GUID)) == 0; +} + +void *find_configuration_table(const EFI_GUID *guid); + +char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path); diff --git a/src/boot/efi/vmm.c b/src/boot/efi/vmm.c new file mode 100644 index 0000000..60e216d --- /dev/null +++ b/src/boot/efi/vmm.c @@ -0,0 +1,426 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if defined(__i386__) || defined(__x86_64__) +# include <cpuid.h> +#endif + +#include "confidential-virt-fundamental.h" +#include "device-path-util.h" +#include "drivers.h" +#include "efi-string.h" +#include "proto/device-path.h" +#include "string-util-fundamental.h" +#include "util.h" +#include "vmm.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, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (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, MAKE_GUID_PTR(QEMU_KERNEL_LOADER_FS_MEDIA), 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; +} + +/* + * Try find ESP when not loaded from ESP + * + * Inspect all filesystems known to the firmware, try find the ESP. In case VMMBootOrderNNNN variables are + * present they are used to inspect the filesystems in the specified order. When nothing was found or the + * variables are not present the function will do one final search pass over all filesystems. + * + * Recent OVMF builds store the qemu boot order (as specified using the bootindex property on the qemu + * command line) in VMMBootOrderNNNN. The variables contain a device path. + * + * Example qemu command line: + * qemu -virtio-scsi-pci,addr=14.0 -device scsi-cd,scsi-id=4,bootindex=1 + * + * Resulting variable: + * VMMBootOrder0000 = PciRoot(0x0)/Pci(0x14,0x0)/Scsi(0x4,0x0) + */ +EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles; + EFI_STATUS err, dp_err; + + assert(ret_vmm_dev); + assert(ret_vmm_dir); + + /* Make sure all file systems have been initialized. Only do this in VMs as this is slow + * on some real firmwares. */ + (void) reconnect_all_drivers(); + + /* find all file system handles */ + err = BS->LocateHandleBuffer( + ByProtocol, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return err; + + for (size_t order = 0;; order++) { + _cleanup_free_ EFI_DEVICE_PATH *dp = NULL; + + _cleanup_free_ char16_t *order_str = xasprintf("VMMBootOrder%04zx", order); + dp_err = efivar_get_raw(MAKE_GUID_PTR(VMM_BOOT_ORDER), order_str, (char **) &dp, NULL); + + for (size_t i = 0; i < n_handles; i++) { + _cleanup_(file_closep) EFI_FILE *root_dir = NULL, *efi_dir = NULL; + EFI_DEVICE_PATH *fs; + + err = BS->HandleProtocol( + handles[i], MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &fs); + if (err != EFI_SUCCESS) + return err; + + /* check against VMMBootOrderNNNN (if set) */ + if (dp_err == EFI_SUCCESS && !device_path_startswith(fs, dp)) + continue; + + err = open_volume(handles[i], &root_dir); + if (err != EFI_SUCCESS) + continue; + + /* simple ESP check */ + err = root_dir->Open(root_dir, &efi_dir, (char16_t*) u"\\EFI", + EFI_FILE_MODE_READ, + EFI_FILE_READ_ONLY | EFI_FILE_DIRECTORY); + if (err != EFI_SUCCESS) + continue; + + *ret_vmm_dev = handles[i]; + *ret_vmm_dir = TAKE_PTR(root_dir); + return EFI_SUCCESS; + } + + if (dp_err != EFI_SUCCESS) + return EFI_NOT_FOUND; + } + assert_not_reached(); +} + +static bool cpuid_in_hypervisor(void) { +#if defined(__i386__) || defined(__x86_64__) + unsigned 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; + + if (FLAGS_SET(ecx, 0x80000000U)) + return true; +#endif + + return false; +} + +#define SMBIOS_TABLE_GUID \ + GUID_DEF(0xeb9d2d31, 0x2d88, 0x11d3, 0x9a, 0x16, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d) +#define SMBIOS3_TABLE_GUID \ + GUID_DEF(0xf2fd1544, 0x9794, 0x4a2c, 0x99, 0x2e, 0xe5, 0xbb, 0xcf, 0x20, 0xe3, 0x94) + +typedef struct { + uint8_t anchor_string[4]; + uint8_t entry_point_structure_checksum; + uint8_t entry_point_length; + uint8_t major_version; + uint8_t minor_version; + uint16_t max_structure_size; + uint8_t entry_point_revision; + uint8_t formatted_area[5]; + uint8_t intermediate_anchor_string[5]; + uint8_t intermediate_checksum; + uint16_t table_length; + uint32_t table_address; + uint16_t number_of_smbios_structures; + uint8_t smbios_bcd_revision; +} _packed_ SmbiosEntryPoint; + +typedef struct { + uint8_t anchor_string[5]; + uint8_t entry_point_structure_checksum; + uint8_t entry_point_length; + uint8_t major_version; + uint8_t minor_version; + uint8_t docrev; + uint8_t entry_point_revision; + uint8_t reserved; + uint32_t table_maximum_size; + uint64_t table_address; +} _packed_ Smbios3EntryPoint; + +typedef struct { + uint8_t type; + uint8_t length; + uint8_t handle[2]; +} _packed_ SmbiosHeader; + +typedef struct { + SmbiosHeader header; + uint8_t vendor; + uint8_t bios_version; + uint16_t bios_segment; + uint8_t bios_release_date; + uint8_t bios_size; + uint64_t bios_characteristics; + uint8_t bios_characteristics_ext[2]; +} _packed_ SmbiosTableType0; + +typedef struct { + SmbiosHeader header; + uint8_t count; + char contents[]; +} _packed_ SmbiosTableType11; + +static const void *find_smbios_configuration_table(uint64_t *ret_size) { + assert(ret_size); + + const Smbios3EntryPoint *entry3 = find_configuration_table(MAKE_GUID_PTR(SMBIOS3_TABLE)); + if (entry3 && memcmp(entry3->anchor_string, "_SM3_", 5) == 0 && + entry3->entry_point_length <= sizeof(*entry3)) { + *ret_size = entry3->table_maximum_size; + return PHYSICAL_ADDRESS_TO_POINTER(entry3->table_address); + } + + const SmbiosEntryPoint *entry = find_configuration_table(MAKE_GUID_PTR(SMBIOS_TABLE)); + if (entry && memcmp(entry->anchor_string, "_SM_", 4) == 0 && + entry->entry_point_length <= sizeof(*entry)) { + *ret_size = entry->table_length; + return PHYSICAL_ADDRESS_TO_POINTER(entry->table_address); + } + + return NULL; +} + +static const SmbiosHeader *get_smbios_table(uint8_t type, uint64_t *ret_size_left) { + uint64_t size = 0; + const uint8_t *p = find_smbios_configuration_table(&size); + if (!p) + return NULL; + + for (;;) { + if (size < sizeof(SmbiosHeader)) + return NULL; + + const SmbiosHeader *header = (const SmbiosHeader *) p; + + /* End of table. */ + if (header->type == 127) + return NULL; + + if (size < header->length) + return NULL; + + if (header->type == type) { + if (ret_size_left) + *ret_size_left = size; + return header; /* Yay! */ + } + + /* Skip over formatted area. */ + size -= header->length; + p += header->length; + + /* Skip over string table. */ + for (;;) { + const uint8_t *e = memchr(p, 0, size); + if (!e) + return NULL; + + if (e == p) {/* Double NUL byte means we've reached the end of the string table. */ + p++; + size--; + break; + } + + size -= e + 1 - p; + p = e + 1; + } + } + + return NULL; +} + +static bool smbios_in_hypervisor(void) { + /* Look up BIOS Information (Type 0). */ + const SmbiosTableType0 *type0 = (const SmbiosTableType0 *) get_smbios_table(0, NULL); + if (!type0 || type0->header.length < sizeof(SmbiosTableType0)) + return false; + + /* Bit 4 of 2nd BIOS characteristics extension bytes indicates virtualization. */ + return FLAGS_SET(type0->bios_characteristics_ext[1], 1 << 4); +} + +bool in_hypervisor(void) { + static int cache = -1; + if (cache >= 0) + return cache; + + cache = cpuid_in_hypervisor() || smbios_in_hypervisor(); + return cache; +} + +const char* smbios_find_oem_string(const char *name) { + uint64_t left; + + assert(name); + + const SmbiosTableType11 *type11 = (const SmbiosTableType11 *) get_smbios_table(11, &left); + if (!type11 || type11->header.length < sizeof(SmbiosTableType11)) + return NULL; + + assert(left >= type11->header.length); + + const char *s = type11->contents; + left -= type11->header.length; + + for (const char *p = s; p < s + left; ) { + const char *e = memchr(p, 0, s + left - p); + if (!e || e == p) /* Double NUL byte means we've reached the end of the OEM strings. */ + break; + + const char *eq = startswith8(p, name); + if (eq && *eq == '=') + return eq + 1; + + p = e + 1; + } + + return NULL; +} + +#if defined(__i386__) || defined(__x86_64__) +static uint32_t cpuid_leaf(uint32_t eax, char ret_sig[static 13], bool swapped) { + /* zero-init as some queries explicitly require subleaf == 0 */ + uint32_t sig[3] = {}; + + if (swapped) + __cpuid_count(eax, 0, eax, sig[0], sig[2], sig[1]); + else + __cpuid_count(eax, 0, eax, sig[0], sig[1], sig[2]); + + memcpy(ret_sig, sig, sizeof(sig)); + ret_sig[12] = 0; /* \0-terminate the string to make string comparison possible */ + + return eax; +} + +static uint64_t msr(uint32_t index) { + uint64_t val; +#ifdef __x86_64__ + uint32_t low, high; + asm volatile ("rdmsr" : "=a"(low), "=d"(high) : "c"(index) : "memory"); + val = ((uint64_t)high << 32) | low; +#else + asm volatile ("rdmsr" : "=A"(val) : "c"(index) : "memory"); +#endif + return val; +} + +static bool detect_hyperv_sev(void) { + uint32_t eax, ebx, ecx, edx, feat; + char sig[13] = {}; + + feat = cpuid_leaf(CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS, sig, false); + + if (feat < CPUID_HYPERV_MIN || feat > CPUID_HYPERV_MAX) + return false; + + if (memcmp(sig, CPUID_SIG_HYPERV, sizeof(sig)) != 0) + return false; + + __cpuid(CPUID_HYPERV_FEATURES, eax, ebx, ecx, edx); + + if (ebx & CPUID_HYPERV_ISOLATION && !(ebx & CPUID_HYPERV_CPU_MANAGEMENT)) { + __cpuid(CPUID_HYPERV_ISOLATION_CONFIG, eax, ebx, ecx, edx); + + if ((ebx & CPUID_HYPERV_ISOLATION_TYPE_MASK) == CPUID_HYPERV_ISOLATION_TYPE_SNP) + return true; + } + + return false; +} + +static bool detect_sev(void) { + uint32_t eax, ebx, ecx, edx; + uint64_t msrval; + + __cpuid(CPUID_GET_HIGHEST_FUNCTION, eax, ebx, ecx, edx); + + if (eax < CPUID_AMD_GET_ENCRYPTED_MEMORY_CAPABILITIES) + return false; + + __cpuid(CPUID_AMD_GET_ENCRYPTED_MEMORY_CAPABILITIES, eax, ebx, ecx, edx); + + /* bit 1 == CPU supports SEV feature + * + * Note, Azure blocks this CPUID leaf from its SEV-SNP + * guests, so we must fallback to trying some HyperV + * specific CPUID checks. + */ + if (!(eax & EAX_SEV)) + return detect_hyperv_sev(); + + msrval = msr(MSR_AMD64_SEV); + + if (msrval & (MSR_SEV_SNP | MSR_SEV_ES | MSR_SEV)) + return true; + + return false; +} + +static bool detect_tdx(void) { + uint32_t eax, ebx, ecx, edx; + char sig[13] = {}; + + __cpuid(CPUID_GET_HIGHEST_FUNCTION, eax, ebx, ecx, edx); + + if (eax < CPUID_INTEL_TDX_ENUMERATION) + return false; + + cpuid_leaf(CPUID_INTEL_TDX_ENUMERATION, sig, true); + + if (memcmp(sig, CPUID_SIG_INTEL_TDX, sizeof(sig)) == 0) + return true; + + return false; +} +#endif /* ! __i386__ && ! __x86_64__ */ + +bool is_confidential_vm(void) { +#if defined(__i386__) || defined(__x86_64__) + char sig[13] = {}; + + if (!cpuid_in_hypervisor()) + return false; + + cpuid_leaf(0, sig, true); + + if (memcmp(sig, CPUID_SIG_AMD, sizeof(sig)) == 0) + return detect_sev(); + if (memcmp(sig, CPUID_SIG_INTEL, sizeof(sig)) == 0) + return detect_tdx(); +#endif /* ! __i386__ && ! __x86_64__ */ + + return false; +} diff --git a/src/boot/efi/vmm.h b/src/boot/efi/vmm.h new file mode 100644 index 0000000..df48af3 --- /dev/null +++ b/src/boot/efi/vmm.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +bool is_direct_boot(EFI_HANDLE device); +EFI_STATUS vmm_open(EFI_HANDLE *ret_qemu_dev, EFI_FILE **ret_qemu_dir); + +bool in_hypervisor(void); + +bool is_confidential_vm(void); + +const char* smbios_find_oem_string(const char *name); diff --git a/src/boot/measure.c b/src/boot/measure.c new file mode 100644 index 0000000..5c5071e --- /dev/null +++ b/src/boot/measure.c @@ -0,0 +1,1085 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "build.h" +#include "efi-loader.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "json.h" +#include "main-func.h" +#include "memstream-util.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 "tpm2-pcr.h" +#include "tpm2-util.h" +#include "uki.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 char *arg_append = 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_DESTRUCTOR_REGISTER(arg_append, freep); + +static 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" + " --append=PATH Load specified JSON signature, and append new signature to it\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" + " --uname=PATH Path to 'uname -r' file %7$s .uname\n" + " --sbat=PATH Path to SBAT file %7$s .sbat\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_UNAME, + ARG_SBAT, + _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, + ARG_APPEND, + }; + + 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 }, + { "uname", required_argument, NULL, ARG_UNAME }, + { "sbat", required_argument, NULL, ARG_SBAT }, + { "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 }, + { "append", required_argument, NULL, ARG_APPEND }, + {} + }; + + 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 ARG_APPEND: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_append); + 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/%i", pcr_states[i].bank, TPM2_PCR_KERNEL_BOOT) < 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 = -EBADF; + 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; + 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_append) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --append= switch is only supported for 'sign', not 'calculate'."); + + assert(!strv_isempty(arg_banks)); + assert(!strv_isempty(arg_phase)); + + r = pcr_states_allocate(&pcr_states); + if (r < 0) + return r; + + size_t n = 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[%i] Phase <%s>%s\n", + ansi_grey(), + TPM2_PCR_KERNEL_BOOT, + isempty(*phase) ? ":" : *phase, + ansi_normal()); + fflush(stderr); + } + + hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); + if (!hd) + return log_oom(); + + printf("%i:%s=%s\n", TPM2_PCR_KERNEL_BOOT, pcr_states[i].bank, hd); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + + array = json_variant_ref(json_variant_by_key(w, pcr_states[i].bank)); + + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!isempty(*phase), "phase", JSON_BUILD_STRING(*phase)), + JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM2_PCR_KERNEL_BOOT)), + 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 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_fclose_ FILE *privkeyf = 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."); + + 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)); + + if (arg_append) { + r = json_parse_file(NULL, arg_append, 0, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s': %m", arg_append); + + if (!json_variant_is_object(v)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "File '%s' is not a valid JSON object, refusing.", arg_append); + } + + /* 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_(memstream_done) MemStream m = {}; + FILE *tf; + + /* No public key was specified, let's derive it automatically, if we can */ + + tf = memstream_init(&m); + 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; + + 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++) { + PcrState *p = pcr_states + i; + + int tpmalg = tpm2_hash_alg_from_string(EVP_MD_name(p->md)); + if (tpmalg < 0) + return log_error_errno(tpmalg, "Unsupported PCR bank"); + + Tpm2PCRValue pcr_value = TPM2_PCR_VALUE_MAKE(TPM2_PCR_KERNEL_BOOT, + tpmalg, + TPM2B_DIGEST_MAKE(p->value, p->value_size)); + + TPM2B_DIGEST pcr_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + + r = tpm2_calculate_policy_pcr(&pcr_value, 1, &pcr_policy_digest); + if (r < 0) + return log_error_errno(r, "Could not calculate PolicyPCR digest: %m"); + + _cleanup_free_ void *sig = NULL; + size_t ss; + + r = digest_and_sign(p->md, privkey, pcr_policy_digest.buffer, pcr_policy_digest.size, &sig, &ss); + if (r < 0) + return log_error_errno(r, "Failed to sign PCR policy: %m"); + + _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) + return r; + + _cleanup_(json_variant_unrefp) JsonVariant *a = NULL; + r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM2_PCR_KERNEL_BOOT, &a); + if (r < 0) + return log_error_errno(r, "Failed to build JSON PCR mask array: %m"); + + _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) + return log_error_errno(r, "Failed to build JSON object: %m"); + + _cleanup_(json_variant_unrefp) JsonVariant *av = NULL; + av = json_variant_ref(json_variant_by_key(v, p->bank)); + + r = json_variant_append_array_nodup(&av, bv); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object: %m"); + + r = json_variant_set_field(&v, p->bank, av); + if (r < 0) + return log_error_errno(r, "Failed to add JSON field: %m"); + } + + /* 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); + + return 0; +} + +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(TPM2_PCR_KERNEL_BOOT, EFI_LOADER_VARIABLE(StubPcrKernelImage), "kernel image"); + if (r < 0) + return r; + + r = compare_reported_pcr_nr(TPM2_PCR_KERNEL_CONFIG, EFI_LOADER_VARIABLE(StubPcrKernelParameters), "kernel parameters"); + if (r < 0) + return r; + + r = compare_reported_pcr_nr(TPM2_PCR_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) { + static const uint32_t relevant_pcrs[] = { + TPM2_PCR_KERNEL_BOOT, + TPM2_PCR_KERNEL_CONFIG, + TPM2_PCR_SYSEXTS, + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + 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]) < 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], + tpm2_pcr_index_to_string(relevant_pcrs[i]), + memeqzero(h, l) ? " (NOT SET!)" : "", + ansi_normal()); + fflush(stderr); + } + + printf("%" PRIu32 ":%s=%s\n", relevant_pcrs[i], 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])), + 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/meson.build b/src/boot/meson.build new file mode 100644 index 0000000..55b9bd6 --- /dev/null +++ b/src/boot/meson.build @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +bootctl_sources = files( + 'bootctl-install.c', + 'bootctl-random-seed.c', + 'bootctl-reboot-to-firmware.c', + 'bootctl-set-efivar.c', + 'bootctl-status.c', + 'bootctl-systemd-efi-options.c', + 'bootctl-uki.c', + 'bootctl-util.c', + 'bootctl.c', +) + +if get_option('link-boot-shared') + boot_link_with = [libshared] +else + boot_link_with = [ + libshared_static, + libsystemd_static, + ] +endif + +executables += [ + executable_template + { + 'name' : 'bootctl', + 'public' : true, + 'conditions' : [ + 'HAVE_BLKID', + ], + 'sources' : bootctl_sources, + 'link_with' : boot_link_with, + 'dependencies' : libblkid, + }, + libexec_template + { + 'name' : 'systemd-bless-boot', + 'public' : true, + 'conditions' : [ + 'HAVE_BLKID', + 'ENABLE_BOOTLOADER', + ], + 'sources' : files('bless-boot.c'), + 'link_with' : boot_link_with, + 'dependencies' : libblkid, + }, + generator_template + { + 'name' : 'systemd-bless-boot-generator', + 'conditions' : [ + 'HAVE_BLKID', + 'ENABLE_BOOTLOADER', + ], + 'sources' : files('bless-boot-generator.c'), + 'link_with' : boot_link_with, + }, + libexec_template + { + 'name' : 'systemd-measure', + 'conditions' : [ + 'HAVE_BLKID', + 'HAVE_OPENSSL', + 'HAVE_TPM2', + ], + 'sources' : files('measure.c'), + 'dependencies' : libopenssl, + }, + libexec_template + { + 'name' : 'systemd-boot-check-no-failures', + 'sources' : files('boot-check-no-failures.c'), + }, +] |