From 037b9a0680b6dd07dec5c2ef1a13da6eabd398d2 Mon Sep 17 00:00:00 2001 From: Clayton Craft Date: Wed, 29 Apr 2026 18:24:21 -0700 Subject: [PATCH] bootctl: back up sd-boot binary to fallback path on update When a primary sd-boot binary already exists on the ESP and is being updated, it is copied to systemd-boot-fallback{arch}.efi before installing the new version. This gives firmware a fallback Boot#### entry pointing to the previous binary in case the new one fails to load. The fallback is preserved (not overwritten) when its product and version match the currently booted bootloader (read from the LoaderInfo EFI variable), since that means it already holds the known good binary that booted this session. In all other cases it is overwritten with the current primary, when no fallback exists yet, when LoaderInfo is unavailable, or when the fallback's product or version differs from what booted. This also moves the version_check() call up so its result determines both the rotation decision and the main copy, and avoids a duplicate check (and duplicate "Skipping..." log) when the binary is already current. --- src/bootctl/bootctl-install.c | 54 ++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index cb46877e6e..b20857375f 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) */