bootctl: add A/B fallback for sd-boot updates (#41650)
On `bootctl install`, two EFI boot entries are registered: one for the primary sd-boot binary and one for a fallback. On `bootctl update`, the existing primary binary is rotated to the fallback path before the new version is installed, so the fallback entry always points to the previous known-good binary. ``` $ sudo bootctl install ... Created EFI boot entry "Linux Boot Manager". Created EFI boot entry "Fallback Linux Boot Manager". $ sudo bootctl update Copied "/boot/EFI/systemd/systemd-bootaa64.efi" to "/boot/EFI/systemd/systemd-boot-fallbackaa64.efi". Copied "/usr/lib/systemd/boot/efi/systemd-bootaa64.efi" to "/boot/EFI/systemd/systemd-bootaa64.efi". $ efibootmgr ... Boot0004* Linux Boot Manager HD(...)/\EFI\systemd\systemd-bootaa64.efi Boot0005* Fallback Linux Boot Manager HD(...)/\EFI\systemd\systemd-boot-fallbackaa64.efi ``` Fixes: #23805
This commit is contained in:
+139
-24
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user