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:
Luca Boccassi
2026-05-21 21:56:05 +01:00
committed by GitHub
+139 -24
View File
@@ -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) {