boot: add support for installing fwupd capsules

fwupd prepares capsules in the ESP and corresponding non-volatile
variables that describe them. It currently ships an EFI binary that
parses and applies them, but the intention is to deprecate it and
move the functionality into bootloaders. A new volatile variable
can be set by a bootloader that supports this workflow, and if
fwupd finds it, it will no longer use its own EFI binary.

Port the implementation from https://github.com/fwupd/fwupd-efi/blob/main/efi/fwupdate.c
and adapt it. The high level flow is defined at
https://github.com/fwupd/fwupd-efi/blob/main/SECURITY.md

Fixes https://github.com/systemd/systemd/issues/36643
This commit is contained in:
Luca Boccassi authored and Zbigniew Jędrzejewski-Szmek committed 2026-11-02 11:07:30 +01:00
1 parent ca50a3cd2f
commit 45b53d7fbf
14 files changed
+726 -60

No files matched your search

+1
View File
@@ -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
+8
View File
@@ -98,6 +98,14 @@
<listitem><para>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.</para></listitem>
<listitem><para>The boot manager can apply UEFI firmware update capsules prepared by
<command>fwupd</command> at boot, without a separate EFI updater. Capsule updates require UEFI 2.0 or
newer firmware. The boot manager sets the
<varname>BootloaderSupportsFwupd</varname> EFI variable to advertise support for the
<ulink url="https://github.com/fwupd/fwupd/tree/main/plugins/uefi-capsule#3rd-party-bootloader">fwupd
bootloader protocol</ulink>. Only capsules on the disk containing the boot manager are processed,
update variables and capsules on other disks are left untouched.</para></listitem>
</itemizedlist>
<para><citerefentry><refentrytitle>bootctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+19 -3
View File
@@ -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. */
+22
View File
@@ -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;
}
+5
View File
@@ -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);
+51
View File
@@ -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;
+1
View File
@@ -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);
+24 -2
View File
@@ -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;
+2 -55
View File
@@ -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))
+581
View File
@@ -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();
}
+9
View File
@@ -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);
+1
View File
@@ -342,6 +342,7 @@ libefi_sources = files(
systemd_boot_sources = files(
'boot.c',
'fwupdate.c',
'line-edit.c',
)
+1
View File
@@ -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;
+1
View File
@@ -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)