diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 77d03e72b6..b609d8f227 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -731,11 +731,57 @@ static int copy_one_file( if (dest_fd < 0 && dest_fd != -ENOENT) return log_error_errno(dest_fd, "Failed to open '%s' under '%s/EFI/systemd' directory: %m", dest_name, j); - /* 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, source_fd, dest_path, dest_parent_fd, dest_name, dest_fd, force); - const char *e = startswith(dest_name, "systemd-boot"); + + /* If a primary sd-boot binary already exists and the source is a newer version, copy + * the existing primary to systemd-boot-fallback{arch}.efi before installing the new + * one, so firmware has a fallback to the previous binary. The fallback is left alone + * when its product and version match the currently booted bootloader (from LoaderInfo), + * so a known good binary stays as the fallback. In all other cases, like no fallback yet, + * LoaderInfo is unavailable, or product/version differs from what booted, it is + * overwritten with the current primary. */ + if (e && dest_fd >= 0 && !force) { + r = version_check(source_fd, source_path, dest_fd, dest_path); + if (r < 0) + /* Stash the error and fall through; the BOOT{arch}.EFI updates below still run. */ + ret = r; + else { + _cleanup_free_ char *fallback_name = strjoin("systemd-boot-fallback", e); + if (!fallback_name) + return log_oom(); + + _cleanup_free_ char *fallback_path = path_join(j, "/EFI/systemd", fallback_name); + if (!fallback_path) + return log_oom(); + + /* Leave the fallback alone if it already holds the currently booted product + * and version, so a known good binary stays as the fallback. If there is no + * fallback yet, LoaderInfo is unavailable, or there is a mismatch, then + * overwrite it with the current primary. */ + bool should_rotate = true; + _cleanup_close_ int fallback_fd = xopenat_full(dest_parent_fd, fallback_name, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (fallback_fd >= 0) { + _cleanup_free_ char *loader_info = NULL, *fallback_version = NULL; + + if (efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderInfo"), &loader_info) >= 0 && + get_file_version(fallback_fd, &fallback_version) >= 0) + should_rotate = compare_product(loader_info, fallback_version) != 0 || + compare_version(loader_info, fallback_version) != 0; + } + + if (should_rotate) { + r = copy_file_with_version_check(dest_path, dest_fd, fallback_path, dest_parent_fd, fallback_name, /* dest_fd= */ -EBADF, /* force= */ true); + if (r < 0) + log_warning_errno(r, "Failed to back up sd-boot binary to fallback path, continuing: %m"); + } + + ret = copy_file_with_version_check(source_path, source_fd, dest_path, dest_parent_fd, dest_name, dest_fd, /* force= */ true); + } + } else + /* 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, source_fd, dest_path, dest_parent_fd, dest_name, dest_fd, force); + if (e) { /* Create the EFI default boot loader name (specified for removable devices) */ @@ -1233,7 +1279,7 @@ static int find_slot(sd_id128_t uuid, const char *path, uint16_t *id) { return 0; } -static int insert_into_order(InstallContext *c, uint16_t slot) { +static int insert_into_order(InstallContext *c, uint16_t slot, uint16_t after_slot) { _cleanup_free_ uint16_t *order = NULL; uint16_t *t; int n; @@ -1255,7 +1301,8 @@ static int insert_into_order(InstallContext *c, uint16_t slot) { continue; /* we do not require to be the first one, all is fine */ - if (c->operation != INSTALL_NEW) + /* if after_slot is set, leave existing position alone to preserve user reordering. */ + if (i == 0 || c->operation != INSTALL_NEW || after_slot != UINT16_MAX) return 0; /* move us to the first slot */ @@ -1264,6 +1311,27 @@ static int insert_into_order(InstallContext *c, uint16_t slot) { return efi_set_boot_order(order, n); } + /* slot is not yet in the order, so insert after a specific slot if requested */ + if (after_slot != UINT16_MAX) { + t = reallocarray(order, n + 1, sizeof(uint16_t)); + if (!t) + return -ENOMEM; + order = t; + + for (int i = 0; i < n; i++) { + if (order[i] != after_slot) + continue; + + memmove(order + i + 2, order + i + 1, (n - i - 1) * sizeof(uint16_t)); + order[i + 1] = slot; + return efi_set_boot_order(order, n + 1); + } + + log_warning("Boot entry %04" PRIx16 " not found in BootOrder, appending new entry at the end.", after_slot); + order[n] = slot; + return efi_set_boot_order(order, n + 1); + } + /* extend array */ t = reallocarray(order, n + 1, sizeof(uint16_t)); if (!t) @@ -1352,14 +1420,20 @@ fallback: return 0; } -static int install_variables( +static int install_boot_option( InstallContext *c, - const char *path) { + const char *path, + const char *description, + bool require_existing, + uint16_t after_slot, + uint16_t *ret_slot) { uint16_t slot; int r; assert(c); + assert(path); + assert(description); if (c->esp_fd < 0) return c->esp_fd; @@ -1375,9 +1449,9 @@ static int install_variables( CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR, F_OK, /* ret_path= */ NULL); - if (r == -ENOENT) + if (r == -ENOENT && require_existing) return 0; - if (r < 0) + if (r < 0 && r != -ENOENT) return log_error_errno(r, "Cannot access \"%s/%s\": %m", j, skip_leading_slash(path)); r = find_slot(c->esp_uuid, path, &slot); @@ -1396,12 +1470,6 @@ static int install_variables( bool existing = r > 0; if (c->operation == INSTALL_NEW || !existing) { - _cleanup_free_ char *description = NULL; - - r = pick_efi_boot_option_description(c->esp_fd, &description); - if (r < 0) - return r; - r = efi_add_boot_option( slot, description, @@ -1424,7 +1492,14 @@ static int install_variables( description); } - return insert_into_order(c, slot); + r = insert_into_order(c, slot, after_slot); + if (r < 0) + return r; + + if (ret_slot) + *ret_slot = slot; + + return 0; } static int are_we_installed(InstallContext *c) { @@ -1535,6 +1610,38 @@ static int load_secure_boot_auto_enroll( } #endif +static int install_variables(InstallContext *c, const char *arch) { + int r; + + assert(c); + + const char *path = strjoina("/EFI/systemd/systemd-boot", arch, ".efi"); + + _cleanup_free_ char *description = NULL; + r = pick_efi_boot_option_description(c->esp_fd, &description); + if (r < 0) + return r; + + uint16_t primary_slot = UINT16_MAX; + r = install_boot_option(c, path, description, /* require_existing= */ true, /* after_slot= */ UINT16_MAX, &primary_slot); + if (r < 0) + return r; + /* If primary registration was skipped (e.g. binary not on ESP), skip the fallback too + * or else it would land at position 0 in BootOrder with no primary ahead of it. */ + if (primary_slot == UINT16_MAX) + return 0; + + const char *fallback_path = strjoina("/EFI/systemd/systemd-boot-fallback", arch, ".efi"); + + _cleanup_free_ char *fallback_description = strjoin("Fallback ", description); + if (!fallback_description) + return log_oom(); + + strshorten(fallback_description, EFI_BOOT_OPTION_DESCRIPTION_MAX); + + return install_boot_option(c, fallback_path, fallback_description, /* require_existing= */ false, /* after_slot= */ primary_slot, /* ret_slot= */ NULL); +} + static int run_install(InstallContext *c) { int r; @@ -1631,8 +1738,7 @@ static int run_install(InstallContext *c) { return 0; } - char *path = strjoina("/EFI/systemd/systemd-boot", arch, ".efi"); - return install_variables(c, path); + return install_variables(c, arch); } int verb_install(int argc, char *argv[], uintptr_t _data, void *userdata) { @@ -1810,7 +1916,7 @@ static int remove_binaries(InstallContext *c) { return RET_GATHER(r, remove_boot_efi(c)); } -static int remove_variables(sd_id128_t uuid, const char *path, bool in_order) { +static int remove_boot_option(sd_id128_t uuid, const char *path, bool in_order) { uint16_t slot; int r; @@ -1858,8 +1964,19 @@ static int remove_loader_variables(void) { return r; } +static int remove_variables(sd_id128_t uuid) { + int r = 0; + + const char *path = strjoina("/EFI/systemd/systemd-boot", get_efi_arch(), ".efi"); + RET_GATHER(r, remove_boot_option(uuid, path, /* in_order= */ true)); + + const char *fallback_path = strjoina("/EFI/systemd/systemd-boot-fallback", get_efi_arch(), ".efi"); + RET_GATHER(r, remove_boot_option(uuid, fallback_path, /* in_order= */ true)); + + return RET_GATHER(r, remove_loader_variables()); +} + int verb_remove(int argc, char *argv[], uintptr_t _data, void *userdata) { - sd_id128_t uuid = SD_ID128_NULL; int r; _cleanup_(install_context_done) InstallContext c = INSTALL_CONTEXT_NULL; @@ -1926,9 +2043,7 @@ int verb_remove(int argc, char *argv[], uintptr_t _data, void *userdata) { return r; } - char *path = strjoina("/EFI/systemd/systemd-boot", get_efi_arch(), ".efi"); - RET_GATHER(r, remove_variables(uuid, path, /* in_order= */ true)); - return RET_GATHER(r, remove_loader_variables()); + return remove_variables(c.esp_uuid); } int verb_is_installed(int argc, char *argv[], uintptr_t _data, void *userdata) {