]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
bootctl: back up sd-boot binary to fallback path on update
authorClayton Craft <clayton@craftyguy.net>
Thu, 30 Apr 2026 01:24:21 +0000 (18:24 -0700)
committerClayton Craft <clayton@craftyguy.net>
Thu, 21 May 2026 18:30:45 +0000 (11:30 -0700)
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

index cb46877e6e1e72637a8265ba26a1237a4ec8f4f2..b20857375f0bdefae2efc947483710c8bd524aea 100644 (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) */