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)