diff --git a/docs/BOOT_LOADER_INTERFACE.md b/docs/BOOT_LOADER_INTERFACE.md
index 8697d038855..25dccb413eb 100644
--- a/docs/BOOT_LOADER_INTERFACE.md
+++ b/docs/BOOT_LOADER_INTERFACE.md
@@ -148,6 +148,7 @@ Variables will be listed below using the Linux efivarfs naming,
* `1 << 21` → The boot loader measures SMBIOS information into a TPM2 PCR and reports the PCR index in the
EFI variable `LoaderPcrSMBIOS-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f`.
* `1 << 22` → The boot loader honours `LoaderFactoryResetVariables` when set.
+ * `1 << 23` → The boot loader supports installing firmware update capsules prepared by fwupd.
* The EFI variable `LoaderFactoryResetVariables-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f`
contains a boolean, formatted as a NUL-terminated UTF-16 string
diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml
index f518c5b0d2b..22ac5aca347 100644
--- a/man/systemd-boot.xml
+++ b/man/systemd-boot.xml
@@ -98,6 +98,14 @@
The boot manager allows for Secure Boot variables to be enrolled if the UEFI firmware is
in setup mode. Additionally, variables can be automatically enrolled if configured.
+
+ The boot manager can apply UEFI firmware update capsules prepared by
+ fwupd at boot, without a separate EFI updater. Capsule updates require UEFI 2.0 or
+ newer firmware. The boot manager sets the
+ BootloaderSupportsFwupd EFI variable to advertise support for the
+ fwupd
+ bootloader protocol. Only capsules on the disk containing the boot manager are processed,
+ update variables and capsules on other disks are left untouched.
bootctl1
diff --git a/src/boot/boot.c b/src/boot/boot.c
index db8ae5d3a4e..4e0f7897422 100644
--- a/src/boot/boot.c
+++ b/src/boot/boot.c
@@ -13,6 +13,7 @@
#include "efivar-erase.h"
#include "efivars.h"
#include "export-vars.h"
+#include "fwupdate.h"
#include "graphics.h"
#include "initrd.h"
#include "iovec-util.h"
@@ -3263,9 +3264,10 @@ static EFI_STATUS secure_boot_discover_keys(Config *config, EFI_FILE *root_dir)
static void export_loader_variables(
EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
- uint64_t init_usec) {
+ uint64_t init_usec,
+ bool fwupdate_available) {
- static const uint64_t loader_features =
+ uint64_t loader_features =
EFI_LOADER_FEATURE_CONFIG_TIMEOUT |
EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT |
EFI_LOADER_FEATURE_ENTRY_DEFAULT |
@@ -3289,6 +3291,7 @@ static void export_loader_variables(
EFI_LOADER_FEATURE_KEYBOARD_LAYOUT |
EFI_LOADER_FEATURE_SMBIOS_MEASURED |
EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES |
+ (fwupdate_available ? EFI_LOADER_FEATURE_FIRMWARE_UPDATE : 0) |
0;
assert(loaded_image);
@@ -3296,6 +3299,13 @@ static void export_loader_variables(
efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeInitUSec", init_usec);
(void) efivar_set_str16(MAKE_GUID_PTR(LOADER), u"LoaderInfo", u"systemd-boot " GIT_VERSION, /* flags= */ 0);
(void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"LoaderFeatures", loader_features, /* flags= */ 0);
+
+ /* Defined by fwupd https://github.com/fwupd/fwupd/tree/main/plugins/uefi-capsule#3rd-party-bootloader */
+ if (FLAGS_SET(loader_features, EFI_LOADER_FEATURE_FIRMWARE_UPDATE)) {
+ uint8_t value = 1;
+ (void) efivar_set_raw(MAKE_GUID_PTR(FWUPDATE), u"BootloaderSupportsFwupd", &value, sizeof(value), /* flags= */ 0);
+ } else
+ (void) efivar_unset(MAKE_GUID_PTR(FWUPDATE), u"BootloaderSupportsFwupd", /* flags= */ 0);
}
static void config_add_system_entries(Config *config) {
@@ -3418,6 +3428,7 @@ static EFI_STATUS discover_root_dir(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, EFI
static EFI_STATUS run(EFI_HANDLE image) {
EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
_cleanup_file_close_ EFI_FILE *root_dir = NULL;
+ _cleanup_free_ EFI_DEVICE_PATH *fwupdate_disk_path = NULL;
_cleanup_(config_free) Config config = {};
EFI_STATUS err;
uint64_t init_usec;
@@ -3432,6 +3443,11 @@ static EFI_STATUS run(EFI_HANDLE image) {
if (err != EFI_SUCCESS)
return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m");
+ /* First, apply any pending fwupd capsules from the boot disk. */
+ err = fwupdate_get_boot_disk_path(loaded_image->DeviceHandle, &fwupdate_disk_path);
+ if (err == EFI_SUCCESS)
+ fwupdate_install(fwupdate_disk_path);
+
err = discover_root_dir(loaded_image, &root_dir);
if (err != EFI_SUCCESS) {
log_error_status(err, "Unable to open root directory: %m");
@@ -3454,7 +3470,7 @@ static EFI_STATUS run(EFI_HANDLE image) {
shim_retain_protocol();
export_common_variables(loaded_image);
- export_loader_variables(loaded_image, init_usec);
+ export_loader_variables(loaded_image, init_usec, fwupdate_disk_path != NULL);
/* Measure SMBIOS data into PCR 1. This is done early, and suppressed if sd-stub later runs in
* the same boot (and vice versa), via the LoaderPcrSMBIOS EFI variable. */
diff --git a/src/boot/device-path-util.c b/src/boot/device-path-util.c
index 73c3933299c..0dd3a3bcfb8 100644
--- a/src/boot/device-path-util.c
+++ b/src/boot/device-path-util.c
@@ -213,3 +213,25 @@ size_t device_path_size(const EFI_DEVICE_PATH *dp) {
EFI_DEVICE_PATH *device_path_dup(const EFI_DEVICE_PATH *dp) {
return xmemdup(ASSERT_PTR(dp), device_path_size(dp));
}
+
+EFI_STATUS device_path_split_file_path(
+ const EFI_DEVICE_PATH *dp,
+ EFI_DEVICE_PATH **ret_device_path,
+ const EFI_DEVICE_PATH **ret_file_path) {
+
+ assert(dp);
+ assert(ret_device_path);
+ assert(ret_file_path);
+
+ /* Return an owned, terminated copy of the device prefix and a borrowed pointer to the file suffix. */
+ for (const EFI_DEVICE_PATH *node = dp; !device_path_is_end(node); node = device_path_next_node(node)) {
+ if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_FILEPATH_DP)
+ continue;
+
+ *ret_device_path = device_path_replace_node(dp, node, /* new_node= */ NULL);
+ *ret_file_path = node;
+ return EFI_SUCCESS;
+ }
+
+ return EFI_NOT_FOUND;
+}
diff --git a/src/boot/device-path-util.h b/src/boot/device-path-util.h
index 8a993f27fe9..06049c05080 100644
--- a/src/boot/device-path-util.h
+++ b/src/boot/device-path-util.h
@@ -38,3 +38,8 @@ size_t device_path_size(const EFI_DEVICE_PATH *dp);
bool device_path_is_valid(const EFI_DEVICE_PATH *dp, size_t size);
EFI_DEVICE_PATH *device_path_dup(const EFI_DEVICE_PATH *dp);
+
+EFI_STATUS device_path_split_file_path(
+ const EFI_DEVICE_PATH *dp,
+ EFI_DEVICE_PATH **ret_device_path,
+ const EFI_DEVICE_PATH **ret_file_path);
diff --git a/src/boot/efi-efivars.c b/src/boot/efi-efivars.c
index 274ff9cfcf5..ed26383dfe4 100644
--- a/src/boot/efi-efivars.c
+++ b/src/boot/efi-efivars.c
@@ -1,10 +1,14 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "efi-efivars.h"
+#include "efi-log.h"
#include "efi-string.h"
#include "ticks.h"
#include "util.h"
+/* Bound enumeration in case firmware never returns EFI_NOT_FOUND. */
+#define ENUMERATE_VARIABLES_MAX 65536U
+
EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, size_t size, uint32_t flags) {
assert(vendor);
assert(name);
@@ -275,6 +279,53 @@ void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t
efivar_set_str16(vendor, name, str, /* flags= */ 0);
}
+EFI_STATUS efivar_get_names(const EFI_GUID *vendor, char16_t ***ret) {
+ _cleanup_strv_free_ char16_t **names = NULL;
+ size_t n = 0, buf_size = 256 * sizeof(char16_t);
+ _cleanup_free_ char16_t *buf = NULL;
+ EFI_GUID guid = {};
+ EFI_STATUS err;
+
+ assert(vendor);
+ assert(ret);
+
+ /* Deleting variables while enumerating them is undefined. Collect the names first so callers can
+ * modify their variables afterwards. */
+ buf = xmalloc(buf_size);
+ buf[0] = 0;
+
+ for (unsigned i = 0;; i++) {
+ if (i >= ENUMERATE_VARIABLES_MAX)
+ return log_debug_status(EFI_ABORTED, "Too many EFI variables, refusing to enumerate further.");
+
+ size_t size = buf_size;
+ err = RT->GetNextVariableName(&size, buf, &guid);
+ if (err == EFI_NOT_FOUND)
+ break;
+ if (err == EFI_BUFFER_TOO_SMALL) {
+ if (size <= buf_size)
+ return log_debug_status(err, "Firmware reported invalid EFI variable name size.");
+
+ /* Retain the current name and GUID: enumeration continues from them. */
+ buf = xrealloc(buf, buf_size, size);
+ buf_size = size;
+ continue;
+ }
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (!efi_guid_equal(&guid, vendor))
+ continue;
+
+ names = xrealloc(names, n == 0 ? 0 : (n + 1) * sizeof(char16_t*), (n + 2) * sizeof(char16_t*));
+ names[n++] = xstrdup16(buf);
+ names[n] = NULL;
+ }
+
+ *ret = TAKE_PTR(names);
+ return EFI_SUCCESS;
+}
+
uint64_t get_os_indications_supported(void) {
uint64_t osind;
EFI_STATUS err;
diff --git a/src/boot/efi-efivars.h b/src/boot/efi-efivars.h
index 7d33f5e225c..48eaae9a351 100644
--- a/src/boot/efi-efivars.h
+++ b/src/boot/efi-efivars.h
@@ -36,5 +36,6 @@ EFI_STATUS efivar_get_boolean_str16(const EFI_GUID *vendor, const char16_t *name
EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret);
EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret);
EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret);
+EFI_STATUS efivar_get_names(const EFI_GUID *vendor, char16_t ***ret);
uint64_t get_os_indications_supported(void);
diff --git a/src/boot/efi.h b/src/boot/efi.h
index d932f3dfdcb..09d99902125 100644
--- a/src/boot/efi.h
+++ b/src/boot/efi.h
@@ -254,6 +254,21 @@ typedef struct {
uint32_t Reserved;
} EFI_TABLE_HEADER;
+typedef struct {
+ EFI_GUID CapsuleGuid;
+ uint32_t HeaderSize;
+ uint32_t Flags;
+ uint32_t CapsuleImageSize;
+} EFI_CAPSULE_HEADER;
+
+typedef struct {
+ uint64_t Length;
+ union {
+ EFI_PHYSICAL_ADDRESS DataBlock;
+ EFI_PHYSICAL_ADDRESS ContinuationPointer;
+ };
+} EFI_CAPSULE_BLOCK_DESCRIPTOR;
+
typedef struct {
EFI_TABLE_HEADER Hdr;
void *RaiseTPL;
@@ -425,8 +440,15 @@ typedef struct {
EFI_STATUS ResetStatus,
size_t DataSize,
void *ResetData);
- void *UpdateCapsule;
- void *QueryCapsuleCapabilities;
+ EFI_STATUS (EFIAPI *UpdateCapsule)(
+ EFI_CAPSULE_HEADER **CapsuleHeaderArray,
+ size_t CapsuleCount,
+ EFI_PHYSICAL_ADDRESS ScatterGatherList);
+ EFI_STATUS (EFIAPI *QueryCapsuleCapabilities)(
+ EFI_CAPSULE_HEADER **CapsuleHeaderArray,
+ size_t CapsuleCount,
+ uint64_t *MaximumCapsuleSize,
+ EFI_RESET_TYPE *ResetType);
void *QueryVariableInfo;
} EFI_RUNTIME_SERVICES;
diff --git a/src/boot/efivar-erase.c b/src/boot/efivar-erase.c
index 07c0bcff54a..82439503316 100644
--- a/src/boot/efivar-erase.c
+++ b/src/boot/efivar-erase.c
@@ -3,64 +3,11 @@
#include "console.h"
#include "efi-efivars.h"
#include "efi-log.h"
-#include "efi-string.h"
#include "efivar-erase.h"
#include "proto/simple-text-io.h"
#include "strv.h"
#include "util.h"
-/* Upper limit on the number of variables we enumerate, as protection against firmware that never returns
- * EFI_NOT_FOUND from GetNextVariableName() */
-#define ENUMERATE_VARIABLES_MAX 65536U
-
-static EFI_STATUS collect_variables(const EFI_GUID *vendor, char16_t ***ret) {
- _cleanup_strv_free_ char16_t **l = NULL;
- size_t n = 0, buf_size = 256 * sizeof(char16_t);
- _cleanup_free_ char16_t *buf = NULL;
- EFI_GUID guid = {};
- EFI_STATUS err;
-
- assert(vendor);
- assert(ret);
-
- /* Note that the UEFI spec leaves undefined what happens if variables are deleted while they are
- * enumerated. Hence we first collect all names, and only delete them afterwards. */
-
- buf = xmalloc(buf_size);
- buf[0] = 0; /* Empty string starts enumeration */
-
- for (unsigned i = 0;; i++) {
- if (i >= ENUMERATE_VARIABLES_MAX)
- return log_error_status(EFI_ABORTED, "Too many EFI variables, refusing to enumerate further.");
-
- size_t size = buf_size;
- err = RT->GetNextVariableName(&size, buf, &guid);
- if (err == EFI_NOT_FOUND)
- break;
- if (err == EFI_BUFFER_TOO_SMALL) {
- if (size <= buf_size)
- return log_error_status(err, "Firmware reported invalid EFI variable name size.");
-
- /* Retain the current name, enumeration continues from it */
- buf = xrealloc(buf, buf_size, size);
- buf_size = size;
- continue;
- }
- if (err != EFI_SUCCESS)
- return log_error_status(err, "Failed to enumerate EFI variables: %m");
-
- if (!efi_guid_equal(&guid, vendor))
- continue;
-
- l = xrealloc(l, n == 0 ? 0 : (n + 1) * sizeof(char16_t*), (n + 2) * sizeof(char16_t*));
- l[n++] = xstrdup16(buf);
- l[n] = NULL;
- }
-
- *ret = TAKE_PTR(l);
- return EFI_SUCCESS;
-}
-
static EFI_STATUS erase_variable(const EFI_GUID *vendor, const char16_t *name) {
uint32_t attributes;
EFI_STATUS err;
@@ -90,9 +37,9 @@ EFI_STATUS efivar_erase_namespace(const EFI_GUID *vendor, const char16_t *const
assert(vendor);
- err = collect_variables(vendor, &l);
+ err = efivar_get_names(vendor, &l);
if (err != EFI_SUCCESS)
- return err;
+ return log_error_status(err, "Failed to enumerate EFI variables: %m");
STRV_FOREACH(i, l) {
if (strv_contains((char16_t**) keep, *i))
diff --git a/src/boot/fwupdate.c b/src/boot/fwupdate.c
new file mode 100644
index 00000000000..4f541a7e017
--- /dev/null
+++ b/src/boot/fwupdate.c
@@ -0,0 +1,581 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* Ported from:
+ * https://github.com/fwupd/fwupd-efi/blob/1a794fc7ce481176926687f4e88e6e0c8d5e9699/efi/fwupdate.c
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "device-path-util.h"
+#include "efi-efivars.h"
+#include "efi-log.h"
+#include "efi-string.h"
+#include "efi.h"
+#include "fwupdate.h"
+#include "memory-util.h"
+#include "proto/block-io.h"
+#include "proto/graphics-output.h"
+#include "string-util.h"
+#include "strv.h"
+#include "util.h"
+
+/* The following types implement the protocol for windows compatible graphical firmware updates. This
+ * extended header is inserted right after the capsule header when the indicated GUID is used.
+ * https://learn.microsoft.com/en-us/windows-hardware/drivers/bringup/boot-screen-components
+ */
+#define UX_CAPSULE_GUID {0x3b8c8162, 0x188c, 0x46a4, {0xae, 0xc9, 0xbe, 0x43, 0xf1, 0xd6, 0x56, 0x97} }
+
+typedef struct {
+ uint8_t version;
+ uint8_t checksum;
+ uint8_t image_type;
+ uint8_t reserved;
+ uint32_t mode;
+ uint32_t x_offset;
+ uint32_t y_offset;
+} _packed_ UX_CAPSULE_HEADER;
+
+/* The following types are defined by fwupd:
+ * https://github.com/fwupd/fwupd-efi/blob/1a794fc7ce481176926687f4e88e6e0c8d5e9699/efi/fwup-efi.h
+ * fwupd uses this struct for the EFI variables it creates in userspace.
+ */
+#define FWUPDATE_INFO_VERSION 7U
+#define FWUPDATE_ATTEMPT_UPDATE 0x00000001
+#define FWUPDATE_ATTEMPTED 0x00000002
+
+typedef struct {
+ uint32_t update_info_version;
+ EFI_GUID guid;
+ uint32_t capsule_flags;
+ uint64_t hw_inst;
+ EFI_TIME time_attempted;
+ uint32_t status;
+ EFI_DEVICE_PATH dp;
+} _packed_ FWUP_UPDATE_INFO;
+
+typedef struct {
+ char16_t *name;
+ uint32_t attrs;
+ size_t size;
+ FWUP_UPDATE_INFO *info;
+ EFI_HANDLE device_handle;
+ const EFI_DEVICE_PATH *file_dp; /* Borrowed from info. */
+} FwupUpdateTable;
+
+static bool fwupdate_supported(void) {
+ /* EFI 1.x runtime tables end at ResetSystem; capsule services were added in UEFI 2.0. */
+ return RT->Hdr.Revision >= (2U << 16) &&
+ RT->Hdr.HeaderSize >= offsetof(EFI_RUNTIME_SERVICES, QueryCapsuleCapabilities) +
+ sizeof(RT->QueryCapsuleCapabilities) &&
+ RT->UpdateCapsule && RT->QueryCapsuleCapabilities;
+}
+
+static FwupUpdateTable *fwup_update_table_free(FwupUpdateTable *update) {
+ if (!update)
+ return NULL;
+
+ free(update->name);
+ free(update->info);
+
+ return mfree(update);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(FwupUpdateTable *, fwup_update_table_free);
+
+static void fwup_update_table_free_many(FwupUpdateTable **a, size_t n) {
+ assert(a || n == 0);
+
+ FOREACH_ARRAY(i, a, n)
+ fwup_update_table_free(*i);
+
+ free(a);
+}
+
+static void capsule_free_many(EFI_CAPSULE_HEADER **a, size_t n) {
+ assert(a || n == 0);
+
+ FOREACH_ARRAY(i, a, n)
+ free(*i);
+
+ free(a);
+}
+
+EFI_STATUS fwupdate_get_boot_disk_path(EFI_HANDLE boot_device, EFI_DEVICE_PATH **ret_disk_path) {
+ EFI_DEVICE_PATH *path = NULL;
+ EFI_STATUS err;
+
+ assert(ret_disk_path);
+
+ if (!fwupdate_supported())
+ return EFI_UNSUPPORTED;
+ if (!boot_device)
+ return EFI_NOT_FOUND;
+
+ err = BS->HandleProtocol(boot_device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &path);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!path)
+ return EFI_NOT_FOUND;
+
+ /* Remove the last partition node, as partition discovery does, to identify the whole disk rather
+ * than just the ESP. An empty prefix cannot identify a disk. */
+ const EFI_DEVICE_PATH *partition = NULL;
+ for (const EFI_DEVICE_PATH *node = path; !device_path_is_end(node); node = device_path_next_node(node))
+ if (node->Type == MEDIA_DEVICE_PATH && IN_SET(node->SubType, MEDIA_HARDDRIVE_DP, MEDIA_CDROM_DP))
+ partition = node;
+ if (!partition || partition == path)
+ return EFI_NOT_FOUND;
+
+ _cleanup_free_ EFI_DEVICE_PATH *disk_path = device_path_replace_node(path, partition, /* new_node= */ NULL);
+ EFI_DEVICE_PATH *remaining = disk_path;
+ EFI_HANDLE disk_handle = NULL;
+
+ err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), &remaining, &disk_handle);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!disk_handle || !remaining || !device_path_is_end(remaining))
+ return EFI_NOT_FOUND;
+
+ EFI_BLOCK_IO_PROTOCOL *block_io = NULL;
+ err = BS->HandleProtocol(disk_handle, MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), (void **) &block_io);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!block_io || !block_io->Media || block_io->Media->LogicalPartition || !block_io->Media->MediaPresent)
+ return EFI_NOT_FOUND;
+
+ /* Fastboot firmware may not have connected the other partitions on this disk yet. */
+ (void) BS->ConnectController(disk_handle, /* DriverImageHandle= */ NULL, /* RemainingDevicePath= */ NULL, /* Recursive= */ true);
+
+ *ret_disk_path = TAKE_PTR(disk_path);
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS parse_info_var(const char16_t *name, FwupUpdateTable **ret_update) {
+ _cleanup_free_ FWUP_UPDATE_INFO *info = NULL;
+ size_t size = 0;
+ uint32_t flags = 0;
+ EFI_STATUS err;
+
+ assert(name);
+ assert(ret_update);
+
+ err = efivar_get_raw_full(MAKE_GUID_PTR(FWUPDATE), name, &flags, (void **) &info, &size);
+ if (err != EFI_SUCCESS)
+ return log_debug_status(err, "Failed to get variable '%ls': %m", name);
+
+ if (size < sizeof(FWUP_UPDATE_INFO))
+ return log_debug_status(EFI_INVALID_PARAMETER, "Variable '%ls' is too small: %zu < %zu", name, size, sizeof(FWUP_UPDATE_INFO));
+ if (info->update_info_version != FWUPDATE_INFO_VERSION)
+ return log_debug_status(EFI_UNSUPPORTED, "Variable '%ls' has unsupported update info version %" PRIu32,
+ name, info->update_info_version);
+
+ EFI_DEVICE_PATH *hdr = &info->dp;
+ size_t dp_size = size - offsetof(FWUP_UPDATE_INFO, dp);
+
+ if (!device_path_is_valid(hdr, dp_size) || device_path_size(hdr) != dp_size)
+ return log_debug_status(EFI_INVALID_PARAMETER, "Variable '%ls' has an invalid EFI device path", name);
+
+ for (const EFI_DEVICE_PATH *node = hdr; !device_path_is_end(node); node = device_path_next_node(node)) {
+ if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_FILEPATH_DP)
+ continue;
+
+ if (node->Length < sizeof(FILEPATH_DEVICE_PATH) + sizeof(char16_t) ||
+ (node->Length - sizeof(FILEPATH_DEVICE_PATH)) % sizeof(char16_t) != 0 ||
+ !memeqzero((const uint8_t *) node + node->Length - sizeof(char16_t), sizeof(char16_t)))
+ return log_debug_status(EFI_INVALID_PARAMETER, "Variable '%ls' has a malformed or unterminated file-path node", name);
+
+ /* Only the file path is passed to the firmware's text converter. Do not expose it to
+ * trailing nodes with unvalidated, type-specific payloads. */
+ if (!device_path_is_end(device_path_next_node(node)))
+ return log_debug_status(EFI_INVALID_PARAMETER, "Variable '%ls' has unexpected nodes after its file path", name);
+ }
+
+ log_debug("Parsed UEFI update variable '%ls': flags=0x%08x, hw_inst=%" PRIu64 ", status=%" PRIu32,
+ name,
+ info->capsule_flags,
+ info->hw_inst,
+ info->status);
+
+ *ret_update = xnew(FwupUpdateTable, 1);
+ **ret_update = (FwupUpdateTable) {
+ .name = xstrdup16(name),
+ .attrs = flags,
+ .size = size,
+ .info = TAKE_PTR(info),
+ };
+
+ return EFI_SUCCESS;
+}
+
+/* Locates the capsule's filesystem on the boot disk as provided by fwupd.
+ *
+ * fwupd's fu_uefi_capsule_device_write_update_info() stores the path built by
+ * fu_uefi_capsule_device_build_dp_buf(): an HD node describing the ESP partition, followed by a File node.
+ * The ESP mount point is stripped from the filename, which is stored as NUL-terminated UTF-16 with
+ * backslashes. fu_efi_device_path_list_write() appends End. No controller/disk hardware prefix is stored.
+ *
+ * An example, with the capsule's ESP mounted at /efi on partition 1 of the PCI disk
+ * (HD(1,...) abbreviates the unchanged ESP partition node, and $F denotes the File node):
+ * filename = /efi/EFI/fedora/fw/fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap
+ * UEFI file path = File(\EFI\fedora\fw\fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap)
+ * update_dp = HD(1,...)/File(\EFI\fedora\fw\fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap)/End
+ * boot_disk_path = PciRoot(0)/Pci(0x4,0)/End
+ * lookup_dp = PciRoot(0)/Pci(0x4,0)/HD(1,...)/End
+ *
+ * Prepending the boot disk's hardware path scopes the partition lookup to that disk, to stop the firmware
+ * from finding the partition on another disk. A full input path must already start with boot_disk_path.
+ * Requires the filesystem lookup to consume lookup_dp completely, rather than accepting a partial match.
+ *
+ * Returns the filesystem handle and, via ret_file_dp, a borrowed pointer to the original file-path nodes
+ * in update_dp (File(\EFI\fedora\fw\fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap)/End in the example). */
+static EFI_STATUS find_capsule(
+ const EFI_DEVICE_PATH *update_dp,
+ const EFI_DEVICE_PATH *boot_disk_path,
+ EFI_HANDLE *ret_device_handle,
+ const EFI_DEVICE_PATH **ret_file_dp) {
+
+ _cleanup_free_ EFI_DEVICE_PATH *device_dp = NULL, *lookup_dp = NULL;
+ const EFI_DEVICE_PATH *file_dp;
+ EFI_HANDLE device_handle = NULL;
+ EFI_STATUS err;
+
+ assert(update_dp);
+ assert(boot_disk_path);
+ assert(!device_path_is_end(boot_disk_path));
+ assert(ret_device_handle);
+ assert(ret_file_dp);
+
+ err = device_path_split_file_path(update_dp, &device_dp, &file_dp);
+ if (err != EFI_SUCCESS)
+ return err;
+
+ if (device_dp->Type == MEDIA_DEVICE_PATH && device_dp->SubType == MEDIA_HARDDRIVE_DP) {
+ if (device_dp->Length != sizeof(HARDDRIVE_DEVICE_PATH) ||
+ !device_path_is_end(device_path_next_node(device_dp)))
+ return EFI_NOT_FOUND;
+
+ lookup_dp = device_path_replace_node(boot_disk_path, /* node= */ NULL, device_dp);
+ } else {
+ if (!device_path_startswith(device_dp, boot_disk_path))
+ return EFI_NOT_FOUND;
+
+ lookup_dp = TAKE_PTR(device_dp);
+ }
+
+ EFI_DEVICE_PATH *remaining = lookup_dp;
+ err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), &remaining, &device_handle);
+ if (err != EFI_SUCCESS)
+ return err;
+ if (!device_handle || !remaining || !device_path_is_end(remaining))
+ return EFI_NOT_FOUND;
+
+ *ret_device_handle = device_handle;
+ *ret_file_dp = file_dp;
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS parse_update_vars(
+ const EFI_DEVICE_PATH *boot_disk_path,
+ FwupUpdateTable ***updates,
+ size_t *n_updates) {
+
+ _cleanup_strv_free_ char16_t **names = NULL;
+ EFI_STATUS err;
+
+ assert(boot_disk_path);
+ assert(updates);
+ assert(n_updates);
+
+ /* A series of fwupd-* named variables will be prepared by userspace, but there is no fixed name,
+ * so we need to iterate over the full table of variables and find them. Unfortunately there are no
+ * APIs to search by GUID, so it's really the entire table that has to be walked.
+ * As per the protocol, start with a NUL string, and then pass the previous name as input to the
+ * next search, as the protocol implementation is stateless. */
+ err = efivar_get_names(MAKE_GUID_PTR(FWUPDATE), &names);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to enumerate firmware update variables: %m");
+
+ STRV_FOREACH(name, names) {
+ if (!startswith_no_case(*name, u"fwupd-"))
+ continue;
+
+ log_debug("Found UEFI update variable '%ls'", *name);
+
+ _cleanup_(fwup_update_table_freep) FwupUpdateTable *update = NULL;
+ err = parse_info_var(*name, &update);
+ if (err != EFI_SUCCESS)
+ /* We cannot establish which disk owns a malformed entry, so leave it untouched. */
+ continue;
+ if (!FLAGS_SET(update->info->status, FWUPDATE_ATTEMPT_UPDATE)) {
+ log_debug("Skipping UEFI update variable '%ls', lacks FWUPDATE_ATTEMPT_UPDATE flag", *name);
+ continue;
+ }
+
+ err = find_capsule(&update->info->dp, boot_disk_path, &update->device_handle, &update->file_dp);
+ if (err != EFI_SUCCESS) {
+ log_debug_status(err, "Ignoring update '%ls', cannot resolve it on the boot disk: %m", *name);
+ continue;
+ }
+
+ if (*n_updates == SIZE_MAX)
+ return log_error_status(EFI_OUT_OF_RESOURCES, "Too many UEFI update variables found");
+
+ *updates = xrealloc(*updates,
+ *n_updates * sizeof(FwupUpdateTable *),
+ (*n_updates + 1) * sizeof(FwupUpdateTable *));
+ (*updates)[(*n_updates)++] = TAKE_PTR(update);
+ }
+
+ return EFI_SUCCESS;
+}
+
+static uint8_t capsule_checksum(const uint8_t *buffer, size_t size) {
+ uint8_t checksum = 0;
+
+ assert(buffer || size == 0);
+
+ FOREACH_ARRAY(c, buffer, size)
+ checksum += *c;
+
+ return checksum;
+}
+
+static EFI_STATUS prepare_capsules(
+ FwupUpdateTable **updates,
+ size_t n_updates,
+ EFI_CAPSULE_HEADER ***ret_capsules,
+ size_t *ret_n_capsules,
+ EFI_CAPSULE_BLOCK_DESCRIPTOR **ret_capsule_blocks) {
+
+ _cleanup_free_ EFI_CAPSULE_BLOCK_DESCRIPTOR *capsule_blocks = NULL;
+ EFI_CAPSULE_HEADER **capsules = NULL;
+ size_t n_capsules = 0;
+ EFI_STATUS err;
+
+ CLEANUP_ARRAY(capsules, n_capsules, capsule_free_many);
+
+ assert(updates || n_updates == 0);
+ assert(ret_capsules);
+ assert(ret_n_capsules);
+ assert(ret_capsule_blocks);
+
+ log_debug("Preparing %zu UEFI capsules...", n_updates);
+
+ FOREACH_ARRAY(u, updates, n_updates) {
+ _cleanup_file_close_ EFI_FILE *root = NULL;
+ err = open_volume((*u)->device_handle, &root);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to open volume for '%ls': %m, skipping", (*u)->name);
+ continue;
+ }
+
+ _cleanup_free_ char16_t *dp_str = NULL;
+ err = device_path_to_str((*u)->file_dp, &dp_str);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to convert device path for '%ls' to string: %m, skipping", (*u)->name);
+ continue;
+ }
+
+ _cleanup_free_ char *file_buffer = NULL;
+ size_t file_size = 0;
+ err = file_read(root, dp_str, /* offset= */ 0, /* size= */ 0, &file_buffer, &file_size);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to read file for '%ls': %m, skipping", (*u)->name);
+ continue;
+ }
+
+ if (file_size < sizeof(EFI_CAPSULE_HEADER)) {
+ log_warning("Capsule for '%ls' is too small for EFI_CAPSULE_HEADER: %zu < %zu, skipping", (*u)->name, file_size, sizeof(EFI_CAPSULE_HEADER));
+ continue;
+ }
+ EFI_CAPSULE_HEADER *capsule = (EFI_CAPSULE_HEADER *)file_buffer;
+
+ if (capsule->HeaderSize < sizeof(EFI_CAPSULE_HEADER) ||
+ capsule->HeaderSize > capsule->CapsuleImageSize ||
+ capsule->CapsuleImageSize > file_size) {
+ log_warning("Invalid capsule sizes for '%ls': header=%" PRIu32 ", image=%" PRIu32 ", file=%zu, skipping",
+ (*u)->name, capsule->HeaderSize, capsule->CapsuleImageSize, file_size);
+ continue;
+ }
+
+ EFI_GUID guid = (*u)->info->guid; /* Avoid unaligned access warning, since info is packed */
+ /* Is this a Microsoft UX capsule? Then let's do special processing for it */
+ if (efi_guid_equal(&guid, MAKE_GUID_PTR(UX_CAPSULE))) {
+ if (capsule->CapsuleImageSize - capsule->HeaderSize < sizeof(UX_CAPSULE_HEADER)) {
+ log_warning("Capsule for '%ls' has too little payload for UX_CAPSULE_HEADER: %" PRIu32 " < %zu, skipping",
+ (*u)->name, capsule->CapsuleImageSize - capsule->HeaderSize, sizeof(UX_CAPSULE_HEADER));
+ continue;
+ }
+
+ EFI_GRAPHICS_OUTPUT_PROTOCOL *graphics = NULL;
+ err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), /* Registration= */ NULL, (void **) &graphics);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to get graphics output protocol for UX capsule '%ls', skipping: %m", (*u)->name);
+ continue;
+ }
+ if (!graphics || !graphics->Mode || !graphics->Mode->Info) {
+ log_warning("No graphics output mode for UX capsule '%ls', skipping", (*u)->name);
+ continue;
+ }
+
+ /* struct is packed, so no alignment check is necessary */
+ UX_CAPSULE_HEADER *hdr = (UX_CAPSULE_HEADER *)((uint8_t *)capsule + capsule->HeaderSize);
+ hdr->mode = graphics->Mode->Mode;
+
+ /* Since the mode is updated, need to recalculate the checksum */
+ hdr->checksum = 0;
+ hdr->checksum = capsule_checksum((uint8_t *) hdr, sizeof(UX_CAPSULE_HEADER));
+ } else if (capsule->Flags == 0)
+ /* fwupd supplies any required flags; do not impose platform-specific reset behavior. */
+ capsule->Flags = (*u)->info->capsule_flags;
+
+ if (n_capsules == SIZE_MAX)
+ return log_error_status(EFI_OUT_OF_RESOURCES, "Too many UEFI capsules");
+
+ EFI_CAPSULE_BLOCK_DESCRIPTOR block_descriptor = {
+ .Length = capsule->CapsuleImageSize,
+ .DataBlock = POINTER_TO_PHYSICAL_ADDRESS(file_buffer),
+ };
+
+ capsule_blocks = xrealloc(capsule_blocks,
+ n_capsules * sizeof(EFI_CAPSULE_BLOCK_DESCRIPTOR),
+ (n_capsules + 1) * sizeof(EFI_CAPSULE_BLOCK_DESCRIPTOR));
+ capsule_blocks[n_capsules] = block_descriptor;
+
+ capsules = xrealloc(capsules,
+ n_capsules * sizeof(EFI_CAPSULE_HEADER *),
+ (n_capsules + 1) * sizeof(EFI_CAPSULE_HEADER *));
+ capsules[n_capsules++] = (EFI_CAPSULE_HEADER *) TAKE_PTR(file_buffer);
+ }
+
+ /* Need to terminate the lists with NULL/empty elements as per spec */
+ capsule_blocks = xrealloc(capsule_blocks,
+ n_capsules * sizeof(EFI_CAPSULE_BLOCK_DESCRIPTOR),
+ (n_capsules + 1) * sizeof(EFI_CAPSULE_BLOCK_DESCRIPTOR));
+ capsule_blocks[n_capsules] = (EFI_CAPSULE_BLOCK_DESCRIPTOR) { };
+ capsules = xrealloc(capsules,
+ n_capsules * sizeof(EFI_CAPSULE_HEADER *),
+ (n_capsules + 1) * sizeof(EFI_CAPSULE_HEADER *));
+ capsules[n_capsules] = NULL;
+
+ *ret_capsule_blocks = TAKE_PTR(capsule_blocks);
+ *ret_capsules = TAKE_PTR(capsules);
+ *ret_n_capsules = n_capsules;
+ n_capsules = 0;
+
+ log_debug("Prepared %zu UEFI capsules for update.", *ret_n_capsules);
+
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS apply_updates(
+ EFI_CAPSULE_HEADER **capsules,
+ size_t n_capsules,
+ const EFI_CAPSULE_BLOCK_DESCRIPTOR *capsule_blocks,
+ EFI_RESET_TYPE *ret_reset_type) {
+
+ EFI_RESET_TYPE reset_type;
+ uint64_t max_capsule_size;
+ EFI_STATUS err;
+
+ assert(capsules && n_capsules > 0);
+ assert(capsule_blocks);
+ assert(ret_reset_type);
+
+ err = RT->QueryCapsuleCapabilities(capsules, n_capsules, &max_capsule_size, &reset_type);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to query capsule capabilities, assuming EfiResetWarm: %m");
+ reset_type = EfiResetWarm;
+ }
+
+ err = RT->UpdateCapsule(capsules, n_capsules, POINTER_TO_PHYSICAL_ADDRESS(capsule_blocks));
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to update capsules: %m");
+
+ *ret_reset_type = reset_type;
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS mark_capsules_as_attempted(FwupUpdateTable **updates, size_t n_updates) {
+ EFI_TIME now;
+ EFI_STATUS err;
+
+ if (n_updates == 0)
+ return EFI_SUCCESS;
+
+ err = RT->GetTime(&now, NULL);
+ if (err != EFI_SUCCESS) /* Should not happen, but sometimes clocks are flaky */
+ now = (EFI_TIME) { };
+
+ FOREACH_ARRAY(u, updates, n_updates) {
+ log_debug("Marking UEFI update variable '%ls' as attempted", (*u)->name);
+
+ (*u)->info->time_attempted = now;
+ (*u)->info->status = FWUPDATE_ATTEMPTED;
+
+ err = efivar_set_raw(MAKE_GUID_PTR(FWUPDATE), (*u)->name, (*u)->info, (*u)->size, (*u)->attrs);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to set variable '%ls': %m", (*u)->name);
+ }
+
+ return EFI_SUCCESS;
+}
+
+/* Implement the flow defined at https://github.com/fwupd/fwupd-efi/blob/main/SECURITY.md */
+void fwupdate_install(const EFI_DEVICE_PATH *boot_disk_path) {
+ EFI_STATUS err;
+
+ assert(boot_disk_path);
+ assert(!device_path_is_end(boot_disk_path));
+
+ if (!fwupdate_supported()) {
+ log_debug("UEFI capsule update services are unavailable, skipping firmware updates.");
+ return;
+ }
+
+ /* Collect only variables whose capsules resolve on the boot disk. Variables for other disks must
+ * neither be loaded nor have their status changed. */
+ FwupUpdateTable **updates = NULL;
+ size_t n_updates = 0;
+ CLEANUP_ARRAY(updates, n_updates, fwup_update_table_free_many);
+ err = parse_update_vars(boot_disk_path, &updates, &n_updates);
+ if (err != EFI_SUCCESS || n_updates == 0)
+ return;
+
+ /* Now prepare the capsules by loading them in memory. This is best-effort: if there are multiple
+ * capsules queued for installation, but some cannot be loaded for any reason, skip them and install
+ * what we can. */
+ _cleanup_free_ EFI_CAPSULE_BLOCK_DESCRIPTOR *capsule_blocks = NULL;
+ EFI_CAPSULE_HEADER **capsules = NULL;
+ size_t n_capsules = 0;
+ CLEANUP_ARRAY(capsules, n_capsules, capsule_free_many);
+ err = prepare_capsules(updates, n_updates, &capsules, &n_capsules, &capsule_blocks);
+ if (err != EFI_SUCCESS)
+ return;
+
+ /* Mark all capsules from the boot disk as "attempted", including the ones that could not be loaded,
+ * otherwise we would just loop forever. */
+ err = mark_capsules_as_attempted(updates, n_updates);
+ if (err != EFI_SUCCESS)
+ return;
+
+ /* If no capsules could be prepared, log and bail out, no need to reset.
+ * NB: this check is placed after the efi var update, since not all efi vars might reference
+ * existing or valid capsules, but the variables need to be marked as processed anyway in order
+ * to avoid getting stuck in a loop. */
+ if (n_capsules == 0)
+ return (void) log_error("No capsules could be prepared out of %zu updates.", n_updates);
+
+ log_info("Applying %zu updates, and then resetting system.", n_capsules);
+ log_wait();
+
+ EFI_RESET_TYPE reset_type = EfiResetWarm;
+ err = apply_updates(capsules, n_capsules, capsule_blocks, &reset_type);
+ if (err != EFI_SUCCESS)
+ return;
+
+ /* The update might already reboot, so might not get to this point. */
+ log_info("Applied %zu updates, resetting system.", n_capsules);
+ log_wait();
+
+ RT->ResetSystem(reset_type, EFI_SUCCESS, /* DataSize= */ 0, /* ResetData= */ NULL);
+ assert_not_reached();
+}
diff --git a/src/boot/fwupdate.h b/src/boot/fwupdate.h
new file mode 100644
index 00000000000..28ae407bf57
--- /dev/null
+++ b/src/boot/fwupdate.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "efi.h"
+
+#define FWUPDATE_GUID { 0x0abba7dc, 0xe516, 0x4167, { 0xbb, 0xf5, 0x4d, 0x9d, 0x1c, 0x73, 0x94, 0x16 } }
+
+EFI_STATUS fwupdate_get_boot_disk_path(EFI_HANDLE boot_device, EFI_DEVICE_PATH **ret_disk_path);
+void fwupdate_install(const EFI_DEVICE_PATH *boot_disk_path);
diff --git a/src/boot/meson.build b/src/boot/meson.build
index c82340205c1..85de3efedf3 100644
--- a/src/boot/meson.build
+++ b/src/boot/meson.build
@@ -342,6 +342,7 @@ libefi_sources = files(
systemd_boot_sources = files(
'boot.c',
+ 'fwupdate.c',
'line-edit.c',
)
diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c
index b6f1d2dead9..4ed4e65e04b 100644
--- a/src/bootctl/bootctl-status.c
+++ b/src/bootctl/bootctl-status.c
@@ -412,6 +412,7 @@ int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
{ EFI_LOADER_FEATURE_KEYBOARD_LAYOUT, "Loader reports firmware keyboard layout" },
{ EFI_LOADER_FEATURE_SMBIOS_MEASURED, "Loader measures SMBIOS information" },
{ EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES, "Loader erases EFI variables on request" },
+ { EFI_LOADER_FEATURE_FIRMWARE_UPDATE, "Support for installing fwupd updates" },
};
static const struct {
uint64_t flag;
diff --git a/src/fundamental/efivars.h b/src/fundamental/efivars.h
index d859b201761..6fc06443f8e 100644
--- a/src/fundamental/efivars.h
+++ b/src/fundamental/efivars.h
@@ -32,6 +32,7 @@
#define EFI_LOADER_FEATURE_KEYBOARD_LAYOUT (UINT64_C(1) << 20)
#define EFI_LOADER_FEATURE_SMBIOS_MEASURED (UINT64_C(1) << 21)
#define EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES (UINT64_C(1) << 22)
+#define EFI_LOADER_FEATURE_FIRMWARE_UPDATE (UINT64_C(1) << 23)
/* Features of the stub, i.e. systemd-stub */
#define EFI_STUB_FEATURE_REPORT_BOOT_PARTITION (UINT64_C(1) << 0)