From 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 22:49:52 +0200 Subject: Adding upstream version 255.4. Signed-off-by: Daniel Baumann --- src/boot/efi/UEFI_SECURITY.md | 122 ++ src/boot/efi/addon.c | 14 + src/boot/efi/bcd.c | 306 ++++ src/boot/efi/bcd.h | 6 + src/boot/efi/boot.c | 2748 +++++++++++++++++++++++++++++++++ src/boot/efi/console.c | 312 ++++ src/boot/efi/console.h | 38 + src/boot/efi/cpio.c | 512 ++++++ src/boot/efi/cpio.h | 31 + src/boot/efi/device-path-util.c | 138 ++ src/boot/efi/device-path-util.h | 27 + src/boot/efi/devicetree.c | 149 ++ src/boot/efi/devicetree.h | 15 + src/boot/efi/drivers.c | 115 ++ src/boot/efi/drivers.h | 11 + src/boot/efi/efi-string.c | 1084 +++++++++++++ src/boot/efi/efi-string.h | 180 +++ src/boot/efi/efi.h | 459 ++++++ src/boot/efi/fuzz-bcd.c | 24 + src/boot/efi/fuzz-efi-osrel.c | 28 + src/boot/efi/fuzz-efi-printf.c | 78 + src/boot/efi/fuzz-efi-string.c | 42 + src/boot/efi/graphics.c | 41 + src/boot/efi/graphics.h | 10 + src/boot/efi/initrd.c | 136 ++ src/boot/efi/initrd.h | 16 + src/boot/efi/linux.c | 156 ++ src/boot/efi/linux.h | 19 + src/boot/efi/linux_x86.c | 224 +++ src/boot/efi/log.c | 115 ++ src/boot/efi/log.h | 32 + src/boot/efi/measure.c | 296 ++++ src/boot/efi/measure.h | 44 + src/boot/efi/meson.build | 409 +++++ src/boot/efi/part-discovery.c | 298 ++++ src/boot/efi/part-discovery.h | 12 + src/boot/efi/pe.c | 332 ++++ src/boot/efi/pe.h | 19 + src/boot/efi/proto/block-io.h | 43 + src/boot/efi/proto/console-control.h | 28 + src/boot/efi/proto/device-path.h | 86 ++ src/boot/efi/proto/dt-fixup.h | 30 + src/boot/efi/proto/file-io.h | 83 + src/boot/efi/proto/graphics-output.h | 78 + src/boot/efi/proto/load-file.h | 21 + src/boot/efi/proto/loaded-image.h | 29 + src/boot/efi/proto/rng.h | 20 + src/boot/efi/proto/security-arch.h | 32 + src/boot/efi/proto/shell-parameters.h | 15 + src/boot/efi/proto/simple-text-io.h | 182 +++ src/boot/efi/proto/tcg.h | 117 ++ src/boot/efi/random-seed.c | 325 ++++ src/boot/efi/random-seed.h | 6 + src/boot/efi/secure-boot.c | 223 +++ src/boot/efi/secure-boot.h | 26 + src/boot/efi/shim.c | 108 ++ src/boot/efi/shim.h | 16 + src/boot/efi/splash.c | 334 ++++ src/boot/efi/splash.h | 6 + src/boot/efi/stub.c | 816 ++++++++++ src/boot/efi/test-bcd.c | 162 ++ src/boot/efi/test-efi-string.c | 794 ++++++++++ src/boot/efi/ticks.c | 111 ++ src/boot/efi/ticks.h | 6 + src/boot/efi/ubsan.c | 46 + src/boot/efi/util.c | 705 +++++++++ src/boot/efi/util.h | 213 +++ src/boot/efi/vmm.c | 426 +++++ src/boot/efi/vmm.h | 13 + 69 files changed, 13698 insertions(+) create mode 100644 src/boot/efi/UEFI_SECURITY.md create mode 100644 src/boot/efi/addon.c create mode 100644 src/boot/efi/bcd.c create mode 100644 src/boot/efi/bcd.h create mode 100644 src/boot/efi/boot.c create mode 100644 src/boot/efi/console.c create mode 100644 src/boot/efi/console.h create mode 100644 src/boot/efi/cpio.c create mode 100644 src/boot/efi/cpio.h create mode 100644 src/boot/efi/device-path-util.c create mode 100644 src/boot/efi/device-path-util.h create mode 100644 src/boot/efi/devicetree.c create mode 100644 src/boot/efi/devicetree.h create mode 100644 src/boot/efi/drivers.c create mode 100644 src/boot/efi/drivers.h create mode 100644 src/boot/efi/efi-string.c create mode 100644 src/boot/efi/efi-string.h create mode 100644 src/boot/efi/efi.h create mode 100644 src/boot/efi/fuzz-bcd.c create mode 100644 src/boot/efi/fuzz-efi-osrel.c create mode 100644 src/boot/efi/fuzz-efi-printf.c create mode 100644 src/boot/efi/fuzz-efi-string.c create mode 100644 src/boot/efi/graphics.c create mode 100644 src/boot/efi/graphics.h create mode 100644 src/boot/efi/initrd.c create mode 100644 src/boot/efi/initrd.h create mode 100644 src/boot/efi/linux.c create mode 100644 src/boot/efi/linux.h create mode 100644 src/boot/efi/linux_x86.c create mode 100644 src/boot/efi/log.c create mode 100644 src/boot/efi/log.h create mode 100644 src/boot/efi/measure.c create mode 100644 src/boot/efi/measure.h create mode 100644 src/boot/efi/meson.build create mode 100644 src/boot/efi/part-discovery.c create mode 100644 src/boot/efi/part-discovery.h create mode 100644 src/boot/efi/pe.c create mode 100644 src/boot/efi/pe.h create mode 100644 src/boot/efi/proto/block-io.h create mode 100644 src/boot/efi/proto/console-control.h create mode 100644 src/boot/efi/proto/device-path.h create mode 100644 src/boot/efi/proto/dt-fixup.h create mode 100644 src/boot/efi/proto/file-io.h create mode 100644 src/boot/efi/proto/graphics-output.h create mode 100644 src/boot/efi/proto/load-file.h create mode 100644 src/boot/efi/proto/loaded-image.h create mode 100644 src/boot/efi/proto/rng.h create mode 100644 src/boot/efi/proto/security-arch.h create mode 100644 src/boot/efi/proto/shell-parameters.h create mode 100644 src/boot/efi/proto/simple-text-io.h create mode 100644 src/boot/efi/proto/tcg.h create mode 100644 src/boot/efi/random-seed.c create mode 100644 src/boot/efi/random-seed.h create mode 100644 src/boot/efi/secure-boot.c create mode 100644 src/boot/efi/secure-boot.h create mode 100644 src/boot/efi/shim.c create mode 100644 src/boot/efi/shim.h create mode 100644 src/boot/efi/splash.c create mode 100644 src/boot/efi/splash.h create mode 100644 src/boot/efi/stub.c create mode 100644 src/boot/efi/test-bcd.c create mode 100644 src/boot/efi/test-efi-string.c create mode 100644 src/boot/efi/ticks.c create mode 100644 src/boot/efi/ticks.h create mode 100644 src/boot/efi/ubsan.c create mode 100644 src/boot/efi/util.c create mode 100644 src/boot/efi/util.h create mode 100644 src/boot/efi/vmm.c create mode 100644 src/boot/efi/vmm.h (limited to 'src/boot/efi') diff --git a/src/boot/efi/UEFI_SECURITY.md b/src/boot/efi/UEFI_SECURITY.md new file mode 100644 index 0000000..9f750d8 --- /dev/null +++ b/src/boot/efi/UEFI_SECURITY.md @@ -0,0 +1,122 @@ +# UEFI Components Security Posture +The systemd project provides a UEFI boot menu, `systemd-boot`, and a stub that can wrap a Linux kernel in a +PE binary, adding various features, `systemd-stub`. These components fully support UEFI SecureBoot, and +this document will describe their security posture and how they comply with industry-standard expectations +for UEFI SecureBoot workflows. + +Note that `systemd-stub` is not the same, or an alternative, to the Linux kernel's own EFI stub. The kernel +stub's role is that of the fundamental entrypoint to kernel execution from UEFI mode, implementing the +modern Linux boot protocol. `systemd-stub` on the other hand loads various resources, including the kernel +image, via the EFI LoadImage/StartImage protocol (although it does support the legacy Linux boot protocol, +as a fallback for older kernels on x86). The purpose of `systemd-stub` is to provide additional features and +functionality for either or both `systemd-boot` and `systemd` (userspace). + +## Fundamental Security Design Goals +The fundamental security design goals for these components are separation of security policy logic from the +rest of the functionality, achieved by offloading security-critical tasks to the firmware or earlier stages +of the boot process (e.g.: `Shim`). + +When SecureBoot is enabled, these components are designed to avoid executing, loading or using +unauthenticated payloads that could compromise the boot process, with special care taken for anything that +could affect the system before `ExitBootServices()` has been called. For example, when additional resources +are loaded, if running with SecureBoot enabled, they will be validated before use. The only exceptions are +the bootloader's own textual configuration files, and parsing metadata out of images for displaying purposes +only. There are no build time or runtime configuration options that can be set to weaken the security model +of these components when SecureBoot is enabled. + +The role of `systemd-boot` is to discover next stage components in the ESP (and XBOOTLDR if present), via +filesystem enumeration or explicit configuration files, and present a menu to the user, to choose the next +step. This auto discovery mechanism is described in details in the [BLS (Boot Loader +Specification)](https://uapi-group.org/specifications/specs/boot_loader_specification/). + +The role of `systemd-stub` is to load and measure in the TPM the post-bootloader stages, such as the kernel, +initrd and kernel command line, and implement optional features such as augmenting the initrd with +additional content such as configuration or optional services. [Unified Kernel +Images](https://uapi-group.org/specifications/specs/unified_kernel_image/) embed `systemd-stub`, a kernel +and other optional components as sections in a PE signed binary, that can thus be executed in UEFI +environments. + +Since it is embedded in a PE signed binary, `systemd-stub` will temporarily disable the UEFI authentication +protocol while loading the payload kernel it wraps, in order to avoid redundant duplicate authentication of +the image, given that the payload kernel was already authenticated and verified as part of the whole image. +SecureBoot authentication is re-enabled immediately after the kernel image has been loaded. + +Various EFI variables, under the vendor UUID `4a67b082-0a4c-41cf-b6c7-440b29bb8c4f`, are set and read by +these components, to pass metadata and configuration between different stages of the boot process, as +defined in the [Boot Loader Interface](https://systemd.io/BOOT_LOADER_INTERFACE/). + +## Dependencies +Neither of these components implements cryptographic primitives, cryptographic checks or drivers. File +access to the ESP is implemented solely via the appropriate UEFI file protocols. Verification of next stage +payloads is implementend solely via the appropriate UEFI image load protocols, which means authenticode +signature checks are again done by the firmware or `Shim`. As a consequence, no external security-critical +libraries (such as OpenSSL or gnu-efi) are used, linked or embedded. + +## Additional Resources +BLS Type #1 entries allow the user to load two types of additional resources that can affect the system +before `ExitBootServices()` has been called, kernel command line arguments and Devicetree blobs, that are +not validated before use, as they do not carry signatures. For this reason, when SecureBoot is enabled, +loading these resources is automatically disabled. There is no override for this security mechanism, neither +at build time nor at runtime. Note that initrds are also not verified in BLS Type #1 configurations, for +compatibility with how SecureBoot has been traditionally handled on Linux-based OSes, as the kernel will +only load them after `ExitBootServices()` has been called. + +Another mechanism is supported by `systemd-boot` and `systemd-stub` to add additional payloads to the boot +process: `addons`. Addons are PE signed binaries that can carry kernel command line arguments or Devicetree +blobs (more might be added in the future). In contrast to the user-specified additions in the Type #1 case +described above, these addons are loaded through the UEFI image loading protocol, and thus are subject to +signature validation, and will be rejected if not signed or if the signature is invalid, following the +standard SecureBoot model. They are also measured in the TPM. + +`systemd-boot` will also load file system drivers that are stored in the ESP, to allow enhancing the +firmware's capabilities. These are again PE signed binaries and will be verified using the appropriate +UEFI protocol. + +A random seed will be loaded and passed to the kernel for early-boot entropy pool filling if found in the +ESP. It is mixed with various other sources of entropy available in the UEFI environment, such as the RNG +protocol, the boot counter and the clock. Moreover, the seed is updated before the kernel is invoked, as +well as after the kernel is invoked (from userspace), with a new seed derived from the Linux kernel entropy +pool. + +When operating as a virtual machine payload, the loaded payloads can be customized via `SMBIOS Type 11 +Strings`, if the hypervisor specifies them. This is automatically disabled if running inside a confidential +computing VM. + +## Certificates Enrollment +When SecureBoot is supported but in `setup` mode, `systemd-boot` can enroll user certificates if a set of +`PK`, `KEK` and `db` certificates is found in the ESP, after which SecureBoot is enabled and a firmware +reset is performed. When running on bare metal, the certificate(s) will be shown to the user on the console, +and manual confirmation will be asked before proceeding. When running as a virtual machine payload, +enrollment is fully automated, without user interaction, unless disabled via a configuration file in the +ESP. The configuration file can also be used to disable enrollment completely. + +## Compiler Hardening +The PE binaries are built with `-fstack-protector-strong`, and the stack canary is seeded with random data if +the UEFI RNG protocol is available. + +The binaries also are linked with `-z relro` and ship with native PE relocations, with the conversion from +ELF performed at build time, instead of containing ELF dynamic relocations, so the image loaded by +firmware/Shim requires fewer writable pages. + +The binaries are linked by default with full LTO support, so no code will be shipped unless it's reachable. + +Finally, the binaries ship with the `NX_COMPAT` bit set. + +The CI infrastructure also employs fuzz testing on various components, including string functions and the +BCD parser. + +## SBAT +`systemd-boot` and `systemd-stub` are built with an `SBAT` section by default. There are build options to +allow customizations of the metadata included in the section, that can be used by downstream distributors. +The `systemd` project will participate in the coordinated `SBAT` disclosure and metadata revision process as +deemed necessary, in coordination with the Shim Review group. + +The upstream project name used to be unified (`systemd`) for both components, but since version v255 has +been split into separate `systemd-boot` and `systemd-stub` project names, so that each component can be +revisioned independently. Most of the code tend to be shared between these two components, but there is no +complete overlap, so it is possible for a vulnerability to affect only one component but not the other. + +## Known Vulnerabilities +There is currently one known (and fixed) security vulnerability affecting `systemd-boot` on arm64 and +riscv64 systems. For details of the affected and fixed versions, please see the [published security +advisory.](https://github.com/systemd/systemd/security/advisories/GHSA-6m6p-rjcq-334c) diff --git a/src/boot/efi/addon.c b/src/boot/efi/addon.c new file mode 100644 index 0000000..95b29da --- /dev/null +++ b/src/boot/efi/addon.c @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "efi.h" +#include "version.h" + +/* Magic string for recognizing our own binaries */ +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-addon " GIT_VERSION " ####"); + +/* This is intended to carry data, not to be executed */ + +EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table); +EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { + return EFI_UNSUPPORTED; +} diff --git a/src/boot/efi/bcd.c b/src/boot/efi/bcd.c new file mode 100644 index 0000000..4533d47 --- /dev/null +++ b/src/boot/efi/bcd.c @@ -0,0 +1,306 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "bcd.h" +#include "efi-string.h" + +enum { + SIG_BASE_BLOCK = 1718052210, /* regf */ + SIG_KEY = 27502, /* nk */ + SIG_SUBKEY_FAST = 26220, /* lf */ + SIG_KEY_VALUE = 27510, /* vk */ +}; + +enum { + REG_SZ = 1, + REG_MULTI_SZ = 7, +}; + +/* These structs contain a lot more members than we care for. They have all + * been squashed into _padN for our convenience. */ + +typedef struct { + uint32_t sig; + uint32_t primary_seqnum; + uint32_t secondary_seqnum; + uint64_t _pad1; + uint32_t version_major; + uint32_t version_minor; + uint32_t type; + uint32_t _pad2; + uint32_t root_cell_offset; + uint64_t _pad3[507]; +} _packed_ BaseBlock; +assert_cc(sizeof(BaseBlock) == 4096); +assert_cc(offsetof(BaseBlock, sig) == 0); +assert_cc(offsetof(BaseBlock, primary_seqnum) == 4); +assert_cc(offsetof(BaseBlock, secondary_seqnum) == 8); +assert_cc(offsetof(BaseBlock, version_major) == 20); +assert_cc(offsetof(BaseBlock, version_minor) == 24); +assert_cc(offsetof(BaseBlock, type) == 28); +assert_cc(offsetof(BaseBlock, root_cell_offset) == 36); + +/* All offsets are relative to the base block and technically point to a hive + * cell struct. But for our use case we don't need to bother about that one, + * so skip over the cell_size uint32_t. */ +#define HIVE_CELL_OFFSET (sizeof(BaseBlock) + 4) + +typedef struct { + uint16_t sig; + uint16_t _pad1[13]; + uint32_t subkeys_offset; + uint32_t _pad2; + uint32_t n_key_values; + uint32_t key_values_offset; + uint32_t _pad3[7]; + uint16_t key_name_len; + uint16_t _pad4; + char key_name[]; +} _packed_ Key; +assert_cc(offsetof(Key, sig) == 0); +assert_cc(offsetof(Key, subkeys_offset) == 28); +assert_cc(offsetof(Key, n_key_values) == 36); +assert_cc(offsetof(Key, key_values_offset) == 40); +assert_cc(offsetof(Key, key_name_len) == 72); +assert_cc(offsetof(Key, key_name) == 76); + +typedef struct { + uint16_t sig; + uint16_t n_entries; + struct SubkeyFastEntry { + uint32_t key_offset; + char name_hint[4]; + } _packed_ entries[]; +} _packed_ SubkeyFast; +assert_cc(offsetof(SubkeyFast, sig) == 0); +assert_cc(offsetof(SubkeyFast, n_entries) == 2); +assert_cc(offsetof(SubkeyFast, entries) == 4); + +typedef struct { + uint16_t sig; + uint16_t name_len; + uint32_t data_size; + uint32_t data_offset; + uint32_t data_type; + uint32_t _pad; + char name[]; +} _packed_ KeyValue; +assert_cc(offsetof(KeyValue, sig) == 0); +assert_cc(offsetof(KeyValue, name_len) == 2); +assert_cc(offsetof(KeyValue, data_size) == 4); +assert_cc(offsetof(KeyValue, data_offset) == 8); +assert_cc(offsetof(KeyValue, data_type) == 12); +assert_cc(offsetof(KeyValue, name) == 20); + +#define BAD_OFFSET(offset, len, max) \ + ((uint64_t) (offset) + (len) >= (max)) + +#define BAD_STRUCT(type, offset, max) \ + ((uint64_t) (offset) + sizeof(type) >= (max)) + +#define BAD_ARRAY(type, array, offset, array_len, max) \ + ((uint64_t) (offset) + offsetof(type, array) + \ + sizeof((type){}.array[0]) * (uint64_t) (array_len) >= (max)) + +static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name); + +static const Key *get_subkey(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) { + assert(bcd); + assert(name); + + if (BAD_STRUCT(SubkeyFast, offset, bcd_len)) + return NULL; + + const SubkeyFast *subkey = (const SubkeyFast *) (bcd + offset); + if (subkey->sig != SIG_SUBKEY_FAST) + return NULL; + + if (BAD_ARRAY(SubkeyFast, entries, offset, subkey->n_entries, bcd_len)) + return NULL; + + for (uint16_t i = 0; i < subkey->n_entries; i++) { + if (!strncaseeq8(name, subkey->entries[i].name_hint, sizeof(subkey->entries[i].name_hint))) + continue; + + const Key *key = get_key(bcd, bcd_len, subkey->entries[i].key_offset, name); + if (key) + return key; + } + + return NULL; +} + +/* We use NUL as registry path separators for convenience. To start from the root, begin + * name with a NUL. Name must end with two NUL. The lookup depth is not restricted, so + * name must be properly validated before calling get_key(). */ +static const Key *get_key(const uint8_t *bcd, uint32_t bcd_len, uint32_t offset, const char *name) { + assert(bcd); + assert(name); + + if (BAD_STRUCT(Key, offset, bcd_len)) + return NULL; + + const Key *key = (const Key *) (bcd + offset); + if (key->sig != SIG_KEY) + return NULL; + + if (BAD_ARRAY(Key, key_name, offset, key->key_name_len, bcd_len)) + return NULL; + + if (*name) { + if (strncaseeq8(name, key->key_name, key->key_name_len) && strlen8(name) == key->key_name_len) + name += key->key_name_len; + else + return NULL; + } + + name++; + return *name ? get_subkey(bcd, bcd_len, key->subkeys_offset, name) : key; +} + +static const KeyValue *get_key_value(const uint8_t *bcd, uint32_t bcd_len, const Key *key, const char *name) { + assert(bcd); + assert(key); + assert(name); + + if (key->n_key_values == 0) + return NULL; + + if (BAD_OFFSET(key->key_values_offset, sizeof(uint32_t) * (uint64_t) key->n_key_values, bcd_len) || + (uintptr_t) (bcd + key->key_values_offset) % alignof(uint32_t) != 0) + return NULL; + + const uint32_t *key_value_list = (const uint32_t *) (bcd + key->key_values_offset); + for (uint32_t i = 0; i < key->n_key_values; i++) { + uint32_t offset = *(key_value_list + i); + + if (BAD_STRUCT(KeyValue, offset, bcd_len)) + continue; + + const KeyValue *kv = (const KeyValue *) (bcd + offset); + if (kv->sig != SIG_KEY_VALUE) + continue; + + if (BAD_ARRAY(KeyValue, name, offset, kv->name_len, bcd_len)) + continue; + + /* If most significant bit is set, data is stored in data_offset itself, but + * we are only interested in UTF16 strings. The only strings that could fit + * would have just one char in it, so let's not bother with this. */ + if (FLAGS_SET(kv->data_size, UINT32_C(1) << 31)) + continue; + + if (BAD_OFFSET(kv->data_offset, kv->data_size, bcd_len)) + continue; + + if (strncaseeq8(name, kv->name, kv->name_len) && strlen8(name) == kv->name_len) + return kv; + } + + return NULL; +} + +/* The BCD store is really just a regular windows registry hive with a rather cryptic internal + * key structure. On a running system it gets mounted to HKEY_LOCAL_MACHINE\BCD00000000. + * + * Of interest to us are these two keys: + * - \Objects\{bootmgr}\Elements\24000001 + * This key is the "displayorder" property and contains a value of type REG_MULTI_SZ + * with the name "Element" that holds a {GUID} list (UTF16, NUL-separated). + * - \Objects\{GUID}\Elements\12000004 + * This key is the "description" property and contains a value of type REG_SZ with the + * name "Element" that holds a NUL-terminated UTF16 string. + * + * The GUIDs and properties are as reported by "bcdedit.exe /v". + * + * To get a title for the BCD store we first look at the displayorder property of {bootmgr} + * (it always has the GUID 9dea862c-5cdd-4e70-acc1-f32b344d4795). If it contains more than + * one GUID, the BCD is multi-boot and we stop looking. Otherwise we take that GUID, look it + * up, and return its description property. */ +char16_t *get_bcd_title(uint8_t *bcd, size_t bcd_len) { + assert(bcd); + + if (HIVE_CELL_OFFSET >= bcd_len) + return NULL; + + BaseBlock *base_block = (BaseBlock *) bcd; + if (base_block->sig != SIG_BASE_BLOCK || + base_block->version_major != 1 || + base_block->version_minor != 3 || + base_block->type != 0 || + base_block->primary_seqnum != base_block->secondary_seqnum) + return NULL; + + bcd += HIVE_CELL_OFFSET; + bcd_len -= HIVE_CELL_OFFSET; + + const Key *objects_key = get_key(bcd, bcd_len, base_block->root_cell_offset, "\0Objects\0"); + if (!objects_key) + return NULL; + + const Key *displayorder_key = get_subkey( + bcd, + bcd_len, + objects_key->subkeys_offset, + "{9dea862c-5cdd-4e70-acc1-f32b344d4795}\0Elements\00024000001\0"); + if (!displayorder_key) + return NULL; + + const KeyValue *displayorder_value = get_key_value(bcd, bcd_len, displayorder_key, "Element"); + if (!displayorder_value) + return NULL; + + char order_guid[sizeof("{00000000-0000-0000-0000-000000000000}\0")]; + if (displayorder_value->data_type != REG_MULTI_SZ || + displayorder_value->data_size != sizeof(char16_t[sizeof(order_guid)]) || + (uintptr_t) (bcd + displayorder_value->data_offset) % alignof(char16_t) != 0) + /* BCD is multi-boot. */ + return NULL; + + /* Keys are stored as ASCII in registry hives if the data fits (and GUIDS always should). */ + char16_t *order_guid_utf16 = (char16_t *) (bcd + displayorder_value->data_offset); + for (size_t i = 0; i < sizeof(order_guid) - 2; i++) { + char16_t c = order_guid_utf16[i]; + switch (c) { + case '-': + case '{': + case '}': + case '0' ... '9': + case 'a' ... 'f': + case 'A' ... 'F': + order_guid[i] = c; + break; + default: + /* Not a valid GUID. */ + return NULL; + } + } + /* Our functions expect the lookup key to be double-derminated. */ + order_guid[sizeof(order_guid) - 2] = '\0'; + order_guid[sizeof(order_guid) - 1] = '\0'; + + const Key *default_key = get_subkey(bcd, bcd_len, objects_key->subkeys_offset, order_guid); + if (!default_key) + return NULL; + + const Key *description_key = get_subkey( + bcd, bcd_len, default_key->subkeys_offset, "Elements\00012000004\0"); + if (!description_key) + return NULL; + + const KeyValue *description_value = get_key_value(bcd, bcd_len, description_key, "Element"); + if (!description_value) + return NULL; + + if (description_value->data_type != REG_SZ || + description_value->data_size < sizeof(char16_t) || + description_value->data_size % sizeof(char16_t) != 0 || + (uintptr_t) (bcd + description_value->data_offset) % alignof(char16_t)) + return NULL; + + /* The data should already be NUL-terminated. */ + char16_t *title = (char16_t *) (bcd + description_value->data_offset); + title[description_value->data_size / sizeof(char16_t) - 1] = '\0'; + return title; +} diff --git a/src/boot/efi/bcd.h b/src/boot/efi/bcd.h new file mode 100644 index 0000000..bb12d89 --- /dev/null +++ b/src/boot/efi/bcd.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +char16_t *get_bcd_title(uint8_t *bcd, size_t bcd_len); diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c new file mode 100644 index 0000000..5c0f0ab --- /dev/null +++ b/src/boot/efi/boot.c @@ -0,0 +1,2748 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bcd.h" +#include "bootspec-fundamental.h" +#include "console.h" +#include "device-path-util.h" +#include "devicetree.h" +#include "drivers.h" +#include "efivars-fundamental.h" +#include "graphics.h" +#include "initrd.h" +#include "linux.h" +#include "measure.h" +#include "part-discovery.h" +#include "pe.h" +#include "proto/block-io.h" +#include "proto/device-path.h" +#include "proto/simple-text-io.h" +#include "random-seed.h" +#include "sbat.h" +#include "secure-boot.h" +#include "shim.h" +#include "ticks.h" +#include "tpm2-pcr.h" +#include "util.h" +#include "version.h" +#include "vmm.h" + +/* Magic string for recognizing our own binaries */ +#define SD_MAGIC "#### LoaderInfo: systemd-boot " GIT_VERSION " ####" +DECLARE_NOALLOC_SECTION(".sdmagic", SD_MAGIC); + +/* Makes systemd-boot available from \EFI\Linux\ for testing purposes. */ +DECLARE_NOALLOC_SECTION( + ".osrel", + "ID=systemd-boot\n" + "VERSION=\"" GIT_VERSION "\"\n" + "NAME=\"systemd-boot " GIT_VERSION "\"\n"); + +DECLARE_SBAT(SBAT_BOOT_SECTION_TEXT); + +typedef enum LoaderType { + LOADER_UNDEFINED, + LOADER_AUTO, + LOADER_EFI, + LOADER_LINUX, /* Boot loader spec type #1 entries */ + LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */ + LOADER_SECURE_BOOT_KEYS, + _LOADER_TYPE_MAX, +} LoaderType; + +typedef struct { + char16_t *id; /* The unique identifier for this entry (typically the filename of the file defining the entry) */ + char16_t *title_show; /* The string to actually display (this is made unique before showing) */ + char16_t *title; /* The raw (human readable) title string of the entry (not necessarily unique) */ + char16_t *sort_key; /* The string to use as primary sort key, usually ID= from os-release, possibly suffixed */ + char16_t *version; /* The raw (human readable) version string of the entry */ + char16_t *machine_id; + EFI_HANDLE *device; + LoaderType type; + char16_t *loader; + char16_t *devicetree; + char16_t *options; + bool options_implied; /* If true, these options are implied if we invoke the PE binary without any parameters (as in: UKI). If false we must specify these options explicitly. */ + char16_t **initrd; + char16_t key; + EFI_STATUS (*call)(void); + int tries_done; + int tries_left; + char16_t *path; + char16_t *current_name; + char16_t *next_name; +} BootEntry; + +typedef struct { + BootEntry **entries; + size_t n_entries; + size_t idx_default; + size_t idx_default_efivar; + uint64_t timeout_sec; /* Actual timeout used (efi_main() override > efivar > config). */ + uint64_t timeout_sec_config; + uint64_t timeout_sec_efivar; + char16_t *entry_default_config; + char16_t *entry_default_efivar; + char16_t *entry_oneshot; + char16_t *entry_saved; + bool editor; + bool auto_entries; + bool auto_firmware; + bool auto_poweroff; + bool auto_reboot; + bool reboot_for_bitlocker; + secure_boot_enroll secure_boot_enroll; + bool force_menu; + bool use_saved_entry; + bool use_saved_entry_efivar; + bool beep; + int64_t console_mode; + int64_t console_mode_efivar; +} Config; + +/* These values have been chosen so that the transitions the user sees could employ unsigned over-/underflow + * like this: + * efivar unset ↔ force menu ↔ no timeout/skip menu ↔ 1 s ↔ 2 s ↔ … + * + * Note: all the values below are ABI, so they are not allowed to change. The bootctl tool sets the numerical + * value of TIMEOUT_MENU_FORCE and TIMEOUT_MENU_HIDDEN, instead of the string for compatibility reasons. + * + * The other values may be set by systemd-boot itself and changing those will lead to functional regression + * when new version of systemd-boot is installed. + * + * All the 64bit values are not ABI and will never be written to an efi variable. + */ +enum { + TIMEOUT_MIN = 1, + TIMEOUT_MAX = UINT32_MAX - 2U, + TIMEOUT_UNSET = UINT32_MAX - 1U, + TIMEOUT_MENU_FORCE = UINT32_MAX, + TIMEOUT_MENU_HIDDEN = 0, + TIMEOUT_TYPE_MAX = UINT32_MAX, + TIMEOUT_MENU_DISABLED = (uint64_t)UINT32_MAX + 1U, + TIMEOUT_TYPE_MAX64 = UINT64_MAX, +}; + +enum { + IDX_MAX = INT16_MAX, + IDX_INVALID, +}; + +static void cursor_left(size_t *cursor, size_t *first) { + assert(cursor); + assert(first); + + if ((*cursor) > 0) + (*cursor)--; + else if ((*first) > 0) + (*first)--; +} + +static void cursor_right(size_t *cursor, size_t *first, size_t x_max, size_t len) { + assert(cursor); + assert(first); + + if ((*cursor)+1 < x_max) + (*cursor)++; + else if ((*first) + (*cursor) < len) + (*first)++; +} + +static bool line_edit(char16_t **line_in, size_t x_max, size_t y_pos) { + _cleanup_free_ char16_t *line = NULL, *print = NULL; + size_t size, len, first = 0, cursor = 0, clear = 0; + + /* Edit the line and return true if it should be executed, false if not. */ + + assert(line_in); + + len = strlen16(*line_in); + size = len + 1024; + line = xnew(char16_t, size); + print = xnew(char16_t, x_max + 1); + strcpy16(line, strempty(*line_in)); + + for (;;) { + EFI_STATUS err; + uint64_t key; + size_t j, cursor_color = EFI_TEXT_ATTR_SWAP(COLOR_EDIT); + + j = MIN(len - first, x_max); + memcpy(print, line + first, j * sizeof(char16_t)); + while (clear > 0 && j < x_max) { + clear--; + print[j++] = ' '; + } + print[j] = '\0'; + + /* See comment at edit_line() call site for why we start at 1. */ + print_at(1, y_pos, COLOR_EDIT, print); + + if (!print[cursor]) + print[cursor] = ' '; + print[cursor+1] = '\0'; + do { + print_at(cursor + 1, y_pos, cursor_color, print + cursor); + cursor_color = EFI_TEXT_ATTR_SWAP(cursor_color); + + err = console_key_read(&key, 750 * 1000); + if (!IN_SET(err, EFI_SUCCESS, EFI_TIMEOUT, EFI_NOT_READY)) + return false; + + print_at(cursor + 1, y_pos, COLOR_EDIT, print + cursor); + } while (err != EFI_SUCCESS); + + switch (key) { + case KEYPRESS(0, SCAN_ESC, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'c'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'g'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('c')): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('g')): + return false; + + case KEYPRESS(0, SCAN_HOME, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'a'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('a')): + /* beginning-of-line */ + cursor = 0; + first = 0; + continue; + + case KEYPRESS(0, SCAN_END, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'e'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('e')): + /* end-of-line */ + cursor = len - first; + if (cursor+1 >= x_max) { + cursor = x_max-1; + first = len - (x_max-1); + } + continue; + + case KEYPRESS(0, SCAN_DOWN, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, 'f'): + case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_RIGHT, 0): + /* forward-word */ + while (line[first + cursor] == ' ') + cursor_right(&cursor, &first, x_max, len); + while (line[first + cursor] && line[first + cursor] != ' ') + cursor_right(&cursor, &first, x_max, len); + continue; + + case KEYPRESS(0, SCAN_UP, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, 'b'): + case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_LEFT, 0): + /* backward-word */ + if ((first + cursor) > 0 && line[first + cursor-1] == ' ') { + cursor_left(&cursor, &first); + while ((first + cursor) > 0 && line[first + cursor] == ' ') + cursor_left(&cursor, &first); + } + while ((first + cursor) > 0 && line[first + cursor-1] != ' ') + cursor_left(&cursor, &first); + continue; + + case KEYPRESS(0, SCAN_RIGHT, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'f'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('f')): + /* forward-char */ + if (first + cursor == len) + continue; + cursor_right(&cursor, &first, x_max, len); + continue; + + case KEYPRESS(0, SCAN_LEFT, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'b'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('b')): + /* backward-char */ + cursor_left(&cursor, &first); + continue; + + case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_DELETE, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, 'd'): + /* kill-word */ + clear = 0; + + size_t k; + for (k = first + cursor; k < len && line[k] == ' '; k++) + clear++; + for (; k < len && line[k] != ' '; k++) + clear++; + + for (size_t i = first + cursor; i + clear < len; i++) + line[i] = line[i + clear]; + len -= clear; + line[len] = '\0'; + continue; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'w'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('w')): + case KEYPRESS(EFI_ALT_PRESSED, 0, '\b'): + /* backward-kill-word */ + clear = 0; + if ((first + cursor) > 0 && line[first + cursor-1] == ' ') { + cursor_left(&cursor, &first); + clear++; + while ((first + cursor) > 0 && line[first + cursor] == ' ') { + cursor_left(&cursor, &first); + clear++; + } + } + while ((first + cursor) > 0 && line[first + cursor-1] != ' ') { + cursor_left(&cursor, &first); + clear++; + } + + for (size_t i = first + cursor; i + clear < len; i++) + line[i] = line[i + clear]; + len -= clear; + line[len] = '\0'; + continue; + + case KEYPRESS(0, SCAN_DELETE, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'd'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('d')): + if (len == 0) + continue; + if (first + cursor == len) + continue; + for (size_t i = first + cursor; i < len; i++) + line[i] = line[i+1]; + clear = 1; + len--; + continue; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'k'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('k')): + /* kill-line */ + line[first + cursor] = '\0'; + clear = len - (first + cursor); + len = first + cursor; + continue; + + case KEYPRESS(0, 0, '\n'): + case KEYPRESS(0, 0, '\r'): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, '\r'): /* Teclast X98+ II firmware sends malformed events */ + if (!streq16(line, *line_in)) { + free(*line_in); + *line_in = TAKE_PTR(line); + } + return true; + + case KEYPRESS(0, 0, '\b'): + if (len == 0) + continue; + if (first == 0 && cursor == 0) + continue; + for (size_t i = first + cursor-1; i < len; i++) + line[i] = line[i+1]; + clear = 1; + len--; + if (cursor > 0) + cursor--; + if (cursor > 0 || first == 0) + continue; + /* show full line if it fits */ + if (len < x_max) { + cursor = first; + first = 0; + continue; + } + /* jump left to see what we delete */ + if (first > 10) { + first -= 10; + cursor = 10; + } else { + cursor = first; + first = 0; + } + continue; + + case KEYPRESS(0, 0, ' ') ... KEYPRESS(0, 0, '~'): + case KEYPRESS(0, 0, 0x80) ... KEYPRESS(0, 0, 0xffff): + if (len+1 == size) + continue; + for (size_t i = len; i > first + cursor; i--) + line[i] = line[i-1]; + line[first + cursor] = KEYCHAR(key); + len++; + line[len] = '\0'; + if (cursor+1 < x_max) + cursor++; + else if (first + cursor < len) + first++; + continue; + } + } +} + +static size_t entry_lookup_key(Config *config, size_t start, char16_t key) { + assert(config); + + if (key == 0) + return IDX_INVALID; + + /* select entry by number key */ + if (key >= '1' && key <= '9') { + size_t i = key - '0'; + if (i > config->n_entries) + i = config->n_entries; + return i-1; + } + + /* find matching key in boot entries */ + for (size_t i = start; i < config->n_entries; i++) + if (config->entries[i]->key == key) + return i; + + for (size_t i = 0; i < start; i++) + if (config->entries[i]->key == key) + return i; + + return IDX_INVALID; +} + +static char16_t* update_timeout_efivar(Config *config, bool inc) { + assert(config); + + switch (config->timeout_sec) { + case TIMEOUT_MAX: + config->timeout_sec = inc ? TIMEOUT_MAX : config->timeout_sec - 1; + break; + case TIMEOUT_UNSET: + config->timeout_sec = inc ? TIMEOUT_MENU_FORCE : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_DISABLED: + config->timeout_sec = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + case TIMEOUT_MENU_FORCE: + config->timeout_sec = inc ? TIMEOUT_MENU_HIDDEN : TIMEOUT_MENU_FORCE; + break; + case TIMEOUT_MENU_HIDDEN: + config->timeout_sec = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + default: + config->timeout_sec = config->timeout_sec + (inc ? 1 : -1); + } + + config->timeout_sec_efivar = config->timeout_sec; + + switch (config->timeout_sec) { + case TIMEOUT_UNSET: + return xstrdup16(u"Menu timeout defined by configuration file."); + case TIMEOUT_MENU_DISABLED: + assert_not_reached(); + case TIMEOUT_MENU_FORCE: + return xstrdup16(u"Timeout disabled, menu will always be shown."); + case TIMEOUT_MENU_HIDDEN: + return xstrdup16(u"Menu hidden. Hold down key at bootup to show menu."); + default: + return xasprintf("Menu timeout set to %u s.", (uint32_t)config->timeout_sec_efivar); + } +} + +static bool unicode_supported(void) { + static int cache = -1; + + if (cache < 0) + /* Basic unicode box drawing support is mandated by the spec, but it does + * not hurt to make sure it works. */ + cache = ST->ConOut->TestString(ST->ConOut, (char16_t *) u"─") == EFI_SUCCESS; + + return cache; +} + +static bool ps_continue(void) { + const char16_t *sep = unicode_supported() ? u"───" : u"---"; + printf("\n%ls Press any key to continue, ESC or q to quit. %ls\n\n", sep, sep); + + uint64_t key; + return console_key_read(&key, UINT64_MAX) == EFI_SUCCESS && + !IN_SET(key, KEYPRESS(0, SCAN_ESC, 0), KEYPRESS(0, 0, 'q'), KEYPRESS(0, 0, 'Q')); +} + +static void print_timeout_status(const char *label, uint64_t t) { + switch (t) { + case TIMEOUT_UNSET: + return; + case TIMEOUT_MENU_DISABLED: + return (void) printf("%s: menu-disabled\n", label); + case TIMEOUT_MENU_FORCE: + return (void) printf("%s: menu-force\n", label); + case TIMEOUT_MENU_HIDDEN: + return (void) printf("%s: menu-hidden\n", label); + default: + return (void) printf("%s: %u s\n", label, (uint32_t)t); + } +} + +static void print_status(Config *config, char16_t *loaded_image_path) { + size_t x_max, y_max; + uint32_t screen_width = 0, screen_height = 0; + SecureBootMode secure; + _cleanup_free_ char16_t *device_part_uuid = NULL; + + assert(config); + + clear_screen(COLOR_NORMAL); + console_query_mode(&x_max, &y_max); + query_screen_resolution(&screen_width, &screen_height); + + secure = secure_boot_mode(); + (void) efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", &device_part_uuid); + + printf(" systemd-boot version: " GIT_VERSION "\n"); + if (loaded_image_path) + printf(" loaded image: %ls\n", loaded_image_path); + if (device_part_uuid) + printf(" loader partition UUID: %ls\n", device_part_uuid); + printf(" architecture: " EFI_MACHINE_TYPE_NAME "\n"); + printf(" UEFI specification: %u.%02u\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + printf(" firmware vendor: %ls\n", ST->FirmwareVendor); + printf(" firmware version: %u.%02u\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + printf(" OS indications: %#" PRIx64 "\n", get_os_indications_supported()); + printf(" secure boot: %ls (%ls)\n", + yes_no(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)), + secure_boot_mode_to_string(secure)); + printf(" shim: %ls\n", yes_no(shim_loaded())); + printf(" TPM: %ls\n", yes_no(tpm_present())); + printf(" console mode: %i/%" PRIi64 " (%zux%zu @%ux%u)\n", + ST->ConOut->Mode->Mode, ST->ConOut->Mode->MaxMode - INT64_C(1), + x_max, y_max, screen_width, screen_height); + + if (!ps_continue()) + return; + + print_timeout_status(" timeout (config)", config->timeout_sec_config); + print_timeout_status(" timeout (EFI var)", config->timeout_sec_efivar); + + if (config->entry_default_config) + printf(" default (config): %ls\n", config->entry_default_config); + if (config->entry_default_efivar) + printf(" default (EFI var): %ls\n", config->entry_default_efivar); + if (config->entry_oneshot) + printf(" default (one-shot): %ls\n", config->entry_oneshot); + if (config->entry_saved) + printf(" saved entry: %ls\n", config->entry_saved); + printf(" editor: %ls\n", yes_no(config->editor)); + printf(" auto-entries: %ls\n", yes_no(config->auto_entries)); + printf(" auto-firmware: %ls\n", yes_no(config->auto_firmware)); + printf(" auto-poweroff: %ls\n", yes_no(config->auto_poweroff)); + printf(" auto-reboot: %ls\n", yes_no(config->auto_reboot)); + printf(" beep: %ls\n", yes_no(config->beep)); + printf(" reboot-for-bitlocker: %ls\n", yes_no(config->reboot_for_bitlocker)); + + switch (config->secure_boot_enroll) { + case ENROLL_OFF: + printf(" secure-boot-enroll: off\n"); + break; + case ENROLL_MANUAL: + printf(" secure-boot-enroll: manual\n"); + break; + case ENROLL_IF_SAFE: + printf(" secure-boot-enroll: if-safe\n"); + break; + case ENROLL_FORCE: + printf(" secure-boot-enroll: force\n"); + break; + default: + assert_not_reached(); + } + + switch (config->console_mode) { + case CONSOLE_MODE_AUTO: + printf(" console-mode (config): auto\n"); + break; + case CONSOLE_MODE_KEEP: + printf(" console-mode (config): keep\n"); + break; + case CONSOLE_MODE_FIRMWARE_MAX: + printf(" console-mode (config): max\n"); + break; + default: + printf(" console-mode (config): %" PRIi64 "\n", config->console_mode); + break; + } + + /* EFI var console mode is always a concrete value or unset. */ + if (config->console_mode_efivar != CONSOLE_MODE_KEEP) + printf("console-mode (EFI var): %" PRIi64 "\n", config->console_mode_efivar); + + if (!ps_continue()) + return; + + for (size_t i = 0; i < config->n_entries; i++) { + BootEntry *entry = config->entries[i]; + EFI_DEVICE_PATH *dp = NULL; + _cleanup_free_ char16_t *dp_str = NULL; + + if (entry->device && + BS->HandleProtocol(entry->device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp) == + EFI_SUCCESS) + (void) device_path_to_str(dp, &dp_str); + + printf(" boot entry: %zu/%zu\n", i + 1, config->n_entries); + printf(" id: %ls\n", entry->id); + if (entry->title) + printf(" title: %ls\n", entry->title); + if (entry->title_show && !streq16(entry->title, entry->title_show)) + printf(" title show: %ls\n", entry->title_show); + if (entry->sort_key) + printf(" sort key: %ls\n", entry->sort_key); + if (entry->version) + printf(" version: %ls\n", entry->version); + if (entry->machine_id) + printf(" machine-id: %ls\n", entry->machine_id); + if (dp_str) + printf(" device: %ls\n", dp_str); + if (entry->loader) + printf(" loader: %ls\n", entry->loader); + STRV_FOREACH(initrd, entry->initrd) + printf(" initrd: %ls\n", *initrd); + if (entry->devicetree) + printf(" devicetree: %ls\n", entry->devicetree); + if (entry->options) + printf(" options: %ls\n", entry->options); + printf(" internal call: %ls\n", yes_no(!!entry->call)); + + printf("counting boots: %ls\n", yes_no(entry->tries_left >= 0)); + if (entry->tries_left >= 0) { + printf(" tries: %i left, %i done\n", entry->tries_left, entry->tries_done); + printf(" current path: %ls\\%ls\n", entry->path, entry->current_name); + printf(" next path: %ls\\%ls\n", entry->path, entry->next_name); + } + + if (!ps_continue()) + return; + } +} + +static EFI_STATUS set_reboot_into_firmware(void) { + uint64_t osind = 0; + EFI_STATUS err; + + (void) efivar_get_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndications", &osind); + osind |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI; + + err = efivar_set_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndications", osind, EFI_VARIABLE_NON_VOLATILE); + if (err != EFI_SUCCESS) + log_error_status(err, "Error setting OsIndications: %m"); + return err; +} + +_noreturn_ static EFI_STATUS poweroff_system(void) { + RT->ResetSystem(EfiResetShutdown, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} + +_noreturn_ static EFI_STATUS reboot_system(void) { + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} + +static EFI_STATUS reboot_into_firmware(void) { + EFI_STATUS err; + + err = set_reboot_into_firmware(); + if (err != EFI_SUCCESS) + return err; + + return reboot_system(); +} + +static bool menu_run( + Config *config, + BootEntry **chosen_entry, + char16_t *loaded_image_path) { + + assert(config); + assert(chosen_entry); + + EFI_STATUS err; + size_t visible_max = 0; + size_t idx_highlight = config->idx_default, idx_highlight_prev = 0; + size_t idx, idx_first = 0, idx_last = 0; + bool new_mode = true, clear = true; + bool refresh = true, highlight = false; + size_t x_start = 0, y_start = 0, y_status = 0, x_max, y_max; + _cleanup_(strv_freep) char16_t **lines = NULL; + _cleanup_free_ char16_t *clearline = NULL, *separator = NULL, *status = NULL; + uint64_t timeout_efivar_saved = config->timeout_sec_efivar; + uint32_t timeout_remain = config->timeout_sec == TIMEOUT_MENU_FORCE ? 0 : config->timeout_sec; + int64_t console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar; + size_t default_efivar_saved = config->idx_default_efivar; + + enum { + ACTION_CONTINUE, /* Continue with loop over user input */ + ACTION_FIRMWARE_SETUP, /* Ask for confirmation and reboot into firmware setup */ + ACTION_POWEROFF, /* Power off the machine */ + ACTION_REBOOT, /* Reboot the machine */ + ACTION_RUN, /* Execute a boot entry */ + ACTION_QUIT, /* Return to the firmware */ + } action = ACTION_CONTINUE; + + graphics_mode(false); + ST->ConIn->Reset(ST->ConIn, false); + ST->ConOut->EnableCursor(ST->ConOut, false); + + /* draw a single character to make ClearScreen work on some firmware */ + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u" "); + + err = console_set_mode(config->console_mode_efivar != CONSOLE_MODE_KEEP ? + config->console_mode_efivar : config->console_mode); + if (err != EFI_SUCCESS) { + clear_screen(COLOR_NORMAL); + log_error_status(err, "Error switching console mode: %m"); + } + + size_t line_width = 0, entry_padding = 3; + while (IN_SET(action, ACTION_CONTINUE, ACTION_FIRMWARE_SETUP)) { + uint64_t key; + + if (new_mode) { + console_query_mode(&x_max, &y_max); + + /* account for padding+status */ + visible_max = y_max - 2; + + /* Drawing entries starts at idx_first until idx_last. We want to make + * sure that idx_highlight is centered, but not if we are close to the + * beginning/end of the entry list. Otherwise we would have a half-empty + * screen. */ + if (config->n_entries <= visible_max || idx_highlight <= visible_max / 2) + idx_first = 0; + else if (idx_highlight >= config->n_entries - (visible_max / 2)) + idx_first = config->n_entries - visible_max; + else + idx_first = idx_highlight - (visible_max / 2); + idx_last = idx_first + visible_max - 1; + + /* length of the longest entry */ + line_width = 0; + for (size_t i = 0; i < config->n_entries; i++) + line_width = MAX(line_width, strlen16(config->entries[i]->title_show)); + line_width = MIN(line_width + 2 * entry_padding, x_max); + + /* offsets to center the entries on the screen */ + x_start = (x_max - (line_width)) / 2; + if (config->n_entries < visible_max) + y_start = ((visible_max - config->n_entries) / 2) + 1; + else + y_start = 0; + + /* Put status line after the entry list, but give it some breathing room. */ + y_status = MIN(y_start + MIN(visible_max, config->n_entries) + 1, y_max - 1); + + lines = strv_free(lines); + clearline = mfree(clearline); + separator = mfree(separator); + + /* menu entries title lines */ + lines = xnew(char16_t *, config->n_entries + 1); + + for (size_t i = 0; i < config->n_entries; i++) { + size_t j, padding; + + lines[i] = xnew(char16_t, line_width + 1); + padding = (line_width - MIN(strlen16(config->entries[i]->title_show), line_width)) / 2; + + for (j = 0; j < padding; j++) + lines[i][j] = ' '; + + for (size_t k = 0; config->entries[i]->title_show[k] != '\0' && j < line_width; j++, k++) + lines[i][j] = config->entries[i]->title_show[k]; + + for (; j < line_width; j++) + lines[i][j] = ' '; + lines[i][line_width] = '\0'; + } + lines[config->n_entries] = NULL; + + clearline = xnew(char16_t, x_max + 1); + separator = xnew(char16_t, x_max + 1); + for (size_t i = 0; i < x_max; i++) { + clearline[i] = ' '; + separator[i] = unicode_supported() ? L'─' : L'-'; + } + clearline[x_max] = 0; + separator[x_max] = 0; + + new_mode = false; + clear = true; + } + + if (clear) { + clear_screen(COLOR_NORMAL); + clear = false; + refresh = true; + } + + if (refresh) { + for (size_t i = idx_first; i <= idx_last && i < config->n_entries; i++) { + print_at(x_start, y_start + i - idx_first, + i == idx_highlight ? COLOR_HIGHLIGHT : COLOR_ENTRY, + lines[i]); + if (i == config->idx_default_efivar) + print_at(x_start, + y_start + i - idx_first, + i == idx_highlight ? COLOR_HIGHLIGHT : COLOR_ENTRY, + unicode_supported() ? u" ►" : u"=>"); + } + refresh = false; + } else if (highlight) { + print_at(x_start, y_start + idx_highlight_prev - idx_first, COLOR_ENTRY, lines[idx_highlight_prev]); + print_at(x_start, y_start + idx_highlight - idx_first, COLOR_HIGHLIGHT, lines[idx_highlight]); + if (idx_highlight_prev == config->idx_default_efivar) + print_at(x_start, + y_start + idx_highlight_prev - idx_first, + COLOR_ENTRY, + unicode_supported() ? u" ►" : u"=>"); + if (idx_highlight == config->idx_default_efivar) + print_at(x_start, + y_start + idx_highlight - idx_first, + COLOR_HIGHLIGHT, + unicode_supported() ? u" ►" : u"=>"); + highlight = false; + } + + if (timeout_remain > 0) { + free(status); + status = xasprintf("Boot in %u s.", timeout_remain); + } + + if (status) { + /* If we draw the last char of the last line, the screen will scroll and break our + * input. Therefore, draw one less character then we could for the status message. + * Note that the same does not apply for the separator line as it will never be drawn + * on the last line. */ + size_t len = strnlen16(status, x_max - 1); + size_t x = (x_max - len) / 2; + status[len] = '\0'; + print_at(0, y_status, COLOR_NORMAL, clearline + x_max - x); + ST->ConOut->OutputString(ST->ConOut, status); + ST->ConOut->OutputString(ST->ConOut, clearline + 1 + x + len); + + len = MIN(MAX(len, line_width) + 2 * entry_padding, x_max); + x = (x_max - len) / 2; + print_at(x, y_status - 1, COLOR_NORMAL, separator + x_max - len); + } else { + print_at(0, y_status - 1, COLOR_NORMAL, clearline); + print_at(0, y_status, COLOR_NORMAL, clearline + 1); /* See comment above. */ + } + + /* Beep several times so that the selected entry can be distinguished. */ + if (config->beep) + beep(idx_highlight + 1); + + err = console_key_read(&key, timeout_remain > 0 ? 1000 * 1000 : UINT64_MAX); + if (err == EFI_NOT_READY) + /* No input device returned a key, try again. This + * normally should not happen. */ + continue; + if (err == EFI_TIMEOUT) { + assert(timeout_remain > 0); + timeout_remain--; + if (timeout_remain == 0) { + action = ACTION_RUN; + break; + } + + /* update status */ + continue; + } + if (err != EFI_SUCCESS) { + action = ACTION_RUN; + break; + } + + timeout_remain = 0; + + /* clear status after keystroke */ + status = mfree(status); + + idx_highlight_prev = idx_highlight; + + if (action == ACTION_FIRMWARE_SETUP) { + if (IN_SET(key, KEYPRESS(0, 0, '\r'), KEYPRESS(0, 0, '\n')) && + set_reboot_into_firmware() == EFI_SUCCESS) + break; + + /* Any key other than newline or a failed attempt cancel the request. */ + action = ACTION_CONTINUE; + continue; + } + + switch (key) { + case KEYPRESS(0, SCAN_UP, 0): + case KEYPRESS(0, 0, 'k'): + case KEYPRESS(0, 0, 'K'): + if (idx_highlight > 0) + idx_highlight--; + break; + + case KEYPRESS(0, SCAN_DOWN, 0): + case KEYPRESS(0, 0, 'j'): + case KEYPRESS(0, 0, 'J'): + if (idx_highlight < config->n_entries-1) + idx_highlight++; + break; + + case KEYPRESS(0, SCAN_HOME, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, '<'): + if (idx_highlight > 0) { + refresh = true; + idx_highlight = 0; + } + break; + + case KEYPRESS(0, SCAN_END, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, '>'): + if (idx_highlight < config->n_entries-1) { + refresh = true; + idx_highlight = config->n_entries-1; + } + break; + + case KEYPRESS(0, SCAN_PAGE_UP, 0): + if (idx_highlight > visible_max) + idx_highlight -= visible_max; + else + idx_highlight = 0; + break; + + case KEYPRESS(0, SCAN_PAGE_DOWN, 0): + idx_highlight += visible_max; + if (idx_highlight > config->n_entries-1) + idx_highlight = config->n_entries-1; + break; + + case KEYPRESS(0, 0, '\n'): + case KEYPRESS(0, 0, '\r'): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, '\r'): /* Teclast X98+ II firmware sends malformed events */ + case KEYPRESS(0, SCAN_RIGHT, 0): + action = ACTION_RUN; + break; + + case KEYPRESS(0, SCAN_F1, 0): + case KEYPRESS(0, 0, 'h'): + case KEYPRESS(0, 0, 'H'): + case KEYPRESS(0, 0, '?'): + /* This must stay below 80 characters! Q/v/Ctrl+l/f deliberately not advertised. */ + status = xasprintf("(d)efault (t/T)imeout (e)dit (r/R)esolution (p)rint %s%s(h)elp", + config->auto_poweroff ? "" : "(O)ff ", + config->auto_reboot ? "" : "re(B)oot "); + break; + + case KEYPRESS(0, 0, 'Q'): + action = ACTION_QUIT; + break; + + case KEYPRESS(0, 0, 'd'): + case KEYPRESS(0, 0, 'D'): + if (config->idx_default_efivar != idx_highlight) { + free(config->entry_default_efivar); + config->entry_default_efivar = xstrdup16(config->entries[idx_highlight]->id); + config->idx_default_efivar = idx_highlight; + status = xstrdup16(u"Default boot entry selected."); + } else { + config->entry_default_efivar = mfree(config->entry_default_efivar); + config->idx_default_efivar = IDX_INVALID; + status = xstrdup16(u"Default boot entry cleared."); + } + config->use_saved_entry_efivar = false; + refresh = true; + break; + + case KEYPRESS(0, 0, '-'): + case KEYPRESS(0, 0, 'T'): + status = update_timeout_efivar(config, false); + break; + + case KEYPRESS(0, 0, '+'): + case KEYPRESS(0, 0, 't'): + status = update_timeout_efivar(config, true); + break; + + case KEYPRESS(0, 0, 'e'): + case KEYPRESS(0, 0, 'E'): + /* only the options of configured entries can be edited */ + if (!config->editor || + !IN_SET(config->entries[idx_highlight]->type, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX)) { + status = xstrdup16(u"Entry does not support editing the command line."); + break; + } + + /* Unified kernels that are signed as a whole will not accept command line options + * when secure boot is enabled unless there is none embedded in the image. Do not try + * to pretend we can edit it to only have it be ignored. */ + if (config->entries[idx_highlight]->type == LOADER_UNIFIED_LINUX && + secure_boot_enabled() && + config->entries[idx_highlight]->options) { + status = xstrdup16(u"Entry not editable in SecureBoot mode."); + break; + } + + /* The edit line may end up on the last line of the screen. And even though we're + * not telling the firmware to advance the line, it still does in this one case, + * causing a scroll to happen that screws with our beautiful boot loader output. + * Since we cannot paint the last character of the edit line, we simply start + * at x-offset 1 for symmetry. */ + print_at(1, y_status, COLOR_EDIT, clearline + 2); + if (line_edit(&config->entries[idx_highlight]->options, x_max - 2, y_status)) + action = ACTION_RUN; + print_at(1, y_status, COLOR_NORMAL, clearline + 2); + + /* The options string was now edited, hence we have to pass it to the invoked + * binary. */ + config->entries[idx_highlight]->options_implied = false; + break; + + case KEYPRESS(0, 0, 'v'): + status = xasprintf( + "systemd-boot " GIT_VERSION " (" EFI_MACHINE_TYPE_NAME "), " + "UEFI Specification %u.%02u, Vendor %ls %u.%02u", + ST->Hdr.Revision >> 16, + ST->Hdr.Revision & 0xffff, + ST->FirmwareVendor, + ST->FirmwareRevision >> 16, + ST->FirmwareRevision & 0xffff); + break; + + case KEYPRESS(0, 0, 'p'): + case KEYPRESS(0, 0, 'P'): + print_status(config, loaded_image_path); + clear = true; + break; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'l'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('l')): + case 'L': /* only uppercase, do not conflict with lower-case 'l' which picks first Linux entry */ + clear = true; + break; + + case KEYPRESS(0, 0, 'r'): + err = console_set_mode(CONSOLE_MODE_NEXT); + if (err != EFI_SUCCESS) + status = xasprintf_status(err, "Error changing console mode: %m"); + else { + config->console_mode_efivar = ST->ConOut->Mode->Mode; + status = xasprintf( + "Console mode changed to %" PRIi64 ".", + config->console_mode_efivar); + } + new_mode = true; + break; + + case KEYPRESS(0, 0, 'R'): + config->console_mode_efivar = CONSOLE_MODE_KEEP; + err = console_set_mode(config->console_mode == CONSOLE_MODE_KEEP ? + console_mode_initial : config->console_mode); + if (err != EFI_SUCCESS) + status = xasprintf_status(err, "Error resetting console mode: %m"); + else + status = xasprintf( + "Console mode reset to %s default.", + config->console_mode == CONSOLE_MODE_KEEP ? + "firmware" : + "configuration file"); + new_mode = true; + break; + + case KEYPRESS(0, 0, 'f'): + case KEYPRESS(0, 0, 'F'): + case KEYPRESS(0, SCAN_F2, 0): /* Most vendors. */ + case KEYPRESS(0, SCAN_F10, 0): /* HP and Lenovo. */ + case KEYPRESS(0, SCAN_DELETE, 0): /* Same as F2. */ + case KEYPRESS(0, SCAN_ESC, 0): /* HP. */ + if (FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) { + action = ACTION_FIRMWARE_SETUP; + /* Let's make sure the user really wants to do this. */ + status = xstrdup16(u"Press Enter to reboot into firmware interface."); + } else + status = xstrdup16(u"Reboot into firmware interface not supported."); + break; + + case KEYPRESS(0, 0, 'O'): /* Only uppercase, so that it can't be hit so easily fat-fingered, + * but still works safely over serial. */ + action = ACTION_POWEROFF; + break; + + case KEYPRESS(0, 0, 'B'): /* ditto */ + action = ACTION_REBOOT; + break; + + default: + /* jump with a hotkey directly to a matching entry */ + idx = entry_lookup_key(config, idx_highlight+1, KEYCHAR(key)); + if (idx == IDX_INVALID) + break; + idx_highlight = idx; + refresh = true; + } + + if (idx_highlight > idx_last) { + idx_last = idx_highlight; + idx_first = 1 + idx_highlight - visible_max; + refresh = true; + } else if (idx_highlight < idx_first) { + idx_first = idx_highlight; + idx_last = idx_highlight + visible_max-1; + refresh = true; + } + + if (!refresh && idx_highlight != idx_highlight_prev) + highlight = true; + } + + /* Update EFI vars after we left the menu to reduce NVRAM writes. */ + + if (default_efivar_saved != config->idx_default_efivar) + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderEntryDefault", config->entry_default_efivar, EFI_VARIABLE_NON_VOLATILE); + + if (console_mode_efivar_saved != config->console_mode_efivar) { + if (config->console_mode_efivar == CONSOLE_MODE_KEEP) + efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigConsoleMode", EFI_VARIABLE_NON_VOLATILE); + else + efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"LoaderConfigConsoleMode", + config->console_mode_efivar, EFI_VARIABLE_NON_VOLATILE); + } + + if (timeout_efivar_saved != config->timeout_sec_efivar) { + switch (config->timeout_sec_efivar) { + case TIMEOUT_UNSET: + efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_DISABLED: + assert_not_reached(); + case TIMEOUT_MENU_FORCE: + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", u"menu-force", EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_HIDDEN: + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", u"menu-hidden", EFI_VARIABLE_NON_VOLATILE); + break; + default: + assert(config->timeout_sec_efivar < UINT32_MAX); + efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeout", + config->timeout_sec_efivar, EFI_VARIABLE_NON_VOLATILE); + } + } + + switch (action) { + case ACTION_CONTINUE: + assert_not_reached(); + case ACTION_POWEROFF: + poweroff_system(); + case ACTION_REBOOT: + case ACTION_FIRMWARE_SETUP: + reboot_system(); + case ACTION_RUN: + case ACTION_QUIT: + break; + } + + *chosen_entry = config->entries[idx_highlight]; + clear_screen(COLOR_NORMAL); + return action == ACTION_RUN; +} + +static void config_add_entry(Config *config, BootEntry *entry) { + assert(config); + assert(entry); + + /* This is just for paranoia. */ + assert(config->n_entries < IDX_MAX); + + if ((config->n_entries & 15) == 0) { + config->entries = xrealloc( + config->entries, + sizeof(void *) * config->n_entries, + sizeof(void *) * (config->n_entries + 16)); + } + config->entries[config->n_entries++] = entry; +} + +static BootEntry* boot_entry_free(BootEntry *entry) { + if (!entry) + return NULL; + + free(entry->id); + free(entry->title_show); + free(entry->title); + free(entry->sort_key); + free(entry->version); + free(entry->machine_id); + free(entry->loader); + free(entry->devicetree); + free(entry->options); + strv_free(entry->initrd); + free(entry->path); + free(entry->current_name); + free(entry->next_name); + + return mfree(entry); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(BootEntry *, boot_entry_free); + +static void config_defaults_load_from_file(Config *config, char *content) { + char *line; + size_t pos = 0; + char *key, *value; + + assert(config); + assert(content); + + while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) + if (streq8(key, "timeout")) { + if (streq8(value, "menu-disabled")) + config->timeout_sec_config = TIMEOUT_MENU_DISABLED; + else if (streq8(value, "menu-force")) + config->timeout_sec_config = TIMEOUT_MENU_FORCE; + else if (streq8(value, "menu-hidden")) + config->timeout_sec_config = TIMEOUT_MENU_HIDDEN; + else { + uint64_t u; + if (!parse_number8(value, &u, NULL) || u > TIMEOUT_TYPE_MAX) { + log_error("Error parsing 'timeout' config option, ignoring: %s", + value); + continue; + } + config->timeout_sec_config = u; + } + config->timeout_sec = config->timeout_sec_config; + + } else if (streq8(key, "default")) { + if (value[0] == '@' && !strcaseeq8(value, "@saved")) { + log_error("Unsupported special entry identifier, ignoring: %s", value); + continue; + } + free(config->entry_default_config); + config->entry_default_config = xstr8_to_16(value); + + } else if (streq8(key, "editor")) { + if (!parse_boolean(value, &config->editor)) + log_error("Error parsing 'editor' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-entries")) { + if (!parse_boolean(value, &config->auto_entries)) + log_error("Error parsing 'auto-entries' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-firmware")) { + if (!parse_boolean(value, &config->auto_firmware)) + log_error("Error parsing 'auto-firmware' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-poweroff")) { + if (!parse_boolean(value, &config->auto_poweroff)) + log_error("Error parsing 'auto-poweroff' config option, ignoring: %s", value); + + } else if (streq8(key, "auto-reboot")) { + if (!parse_boolean(value, &config->auto_reboot)) + log_error("Error parsing 'auto-reboot' config option, ignoring: %s", value); + + } else if (streq8(key, "beep")) { + if (!parse_boolean(value, &config->beep)) + log_error("Error parsing 'beep' config option, ignoring: %s", value); + + } else if (streq8(key, "reboot-for-bitlocker")) { + if (!parse_boolean(value, &config->reboot_for_bitlocker)) + log_error("Error parsing 'reboot-for-bitlocker' config option, ignoring: %s", + value); + + } else if (streq8(key, "secure-boot-enroll")) { + if (streq8(value, "manual")) + config->secure_boot_enroll = ENROLL_MANUAL; + else if (streq8(value, "force")) + config->secure_boot_enroll = ENROLL_FORCE; + else if (streq8(value, "if-safe")) + config->secure_boot_enroll = ENROLL_IF_SAFE; + else if (streq8(value, "off")) + config->secure_boot_enroll = ENROLL_OFF; + else + log_error("Error parsing 'secure-boot-enroll' config option, ignoring: %s", + value); + + } else if (streq8(key, "console-mode")) { + if (streq8(value, "auto")) + config->console_mode = CONSOLE_MODE_AUTO; + else if (streq8(value, "max")) + config->console_mode = CONSOLE_MODE_FIRMWARE_MAX; + else if (streq8(value, "keep")) + config->console_mode = CONSOLE_MODE_KEEP; + else { + uint64_t u; + if (!parse_number8(value, &u, NULL) || u > CONSOLE_MODE_RANGE_MAX) { + log_error("Error parsing 'console-mode' config option, ignoring: %s", + value); + continue; + } + config->console_mode = u; + } + } +} + +static void boot_entry_parse_tries( + BootEntry *entry, + const char16_t *path, + const char16_t *file, + const char16_t *suffix) { + + assert(entry); + assert(path); + assert(file); + assert(suffix); + + /* + * Parses a suffix of two counters (one going down, one going up) in the form "+LEFT-DONE" from the end of the + * filename (but before the .efi/.conf suffix), where the "-DONE" part is optional and may be left out (in + * which case that counter as assumed to be zero, i.e. the missing part is synonymous to "-0"). + * + * Names we grok, and the series they result in: + * + * foobar+3.efi → foobar+2-1.efi → foobar+1-2.efi → foobar+0-3.efi → STOP! + * foobar+4-0.efi → foobar+3-1.efi → foobar+2-2.efi → foobar+1-3.efi → foobar+0-4.efi → STOP! + */ + + const char16_t *counter = NULL; + for (;;) { + char16_t *plus = strchr16(counter ?: file, '+'); + if (plus) { + /* We want the last "+". */ + counter = plus + 1; + continue; + } + if (counter) + break; + + /* No boot counter found. */ + return; + } + + uint64_t tries_left, tries_done = 0; + size_t prefix_len = counter - file; + + if (!parse_number16(counter, &tries_left, &counter) || tries_left > INT_MAX) + return; + + /* Parse done counter only if present. */ + if (*counter == '-' && (!parse_number16(counter + 1, &tries_done, &counter) || tries_done > INT_MAX)) + return; + + /* Boot counter in the middle of the name? */ + if (!streq16(counter, suffix)) + return; + + entry->tries_left = tries_left; + entry->tries_done = tries_done; + entry->path = xstrdup16(path); + entry->current_name = xstrdup16(file); + entry->next_name = xasprintf( + "%.*ls%" PRIu64 "-%" PRIu64 "%ls", + (int) prefix_len, + file, + LESS_BY(tries_left, 1u), + MIN(tries_done + 1, (uint64_t) INT_MAX), + suffix); +} + +static EFI_STATUS boot_entry_bump_counters(BootEntry *entry) { + _cleanup_free_ char16_t* old_path = NULL, *new_path = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + _cleanup_free_ EFI_FILE_INFO *file_info = NULL; + size_t file_info_size; + EFI_STATUS err; + + assert(entry); + + if (entry->tries_left < 0) + return EFI_SUCCESS; + + if (!entry->path || !entry->current_name || !entry->next_name) + return EFI_SUCCESS; + + _cleanup_(file_closep) EFI_FILE *root = NULL; + err = open_volume(entry->device, &root); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error opening entry root path: %m"); + + old_path = xasprintf("%ls\\%ls", entry->path, entry->current_name); + + err = root->Open(root, &handle, old_path, EFI_FILE_MODE_READ|EFI_FILE_MODE_WRITE, 0ULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error opening boot entry: %m"); + + err = get_file_info(handle, &file_info, &file_info_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting boot entry file info: %m"); + + /* And rename the file */ + strcpy16(file_info->FileName, entry->next_name); + err = handle->SetInfo(handle, MAKE_GUID_PTR(EFI_FILE_INFO), file_info_size, file_info); + if (err != EFI_SUCCESS) + return log_error_status( + err, "Failed to rename '%ls' to '%ls', ignoring: %m", old_path, entry->next_name); + + /* Flush everything to disk, just in case… */ + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error flushing boot entry file info: %m"); + + /* Let's tell the OS that we renamed this file, so that it knows what to rename to the counter-less name on + * success */ + new_path = xasprintf("%ls\\%ls", entry->path, entry->next_name); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderBootCountPath", new_path, 0); + + /* If the file we just renamed is the loader path, then let's update that. */ + if (streq16(entry->loader, old_path)) { + free(entry->loader); + entry->loader = TAKE_PTR(new_path); + } + + return EFI_SUCCESS; +} + +static void boot_entry_add_type1( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir, + const char16_t *path, + const char16_t *file, + char *content, + const char16_t *loaded_image_path) { + + _cleanup_(boot_entry_freep) BootEntry *entry = NULL; + char *line; + size_t pos = 0, n_initrd = 0; + char *key, *value; + EFI_STATUS err; + + assert(config); + assert(device); + assert(root_dir); + assert(path); + assert(file); + assert(content); + + entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .tries_done = -1, + .tries_left = -1, + }; + + while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) + if (streq8(key, "title")) { + free(entry->title); + entry->title = xstr8_to_16(value); + + } else if (streq8(key, "sort-key")) { + free(entry->sort_key); + entry->sort_key = xstr8_to_16(value); + + } else if (streq8(key, "version")) { + free(entry->version); + entry->version = xstr8_to_16(value); + + } else if (streq8(key, "machine-id")) { + free(entry->machine_id); + entry->machine_id = xstr8_to_16(value); + + } else if (streq8(key, "linux")) { + free(entry->loader); + entry->type = LOADER_LINUX; + entry->loader = xstr8_to_path(value); + entry->key = 'l'; + + } else if (streq8(key, "efi")) { + entry->type = LOADER_EFI; + free(entry->loader); + entry->loader = xstr8_to_path(value); + + /* do not add an entry for ourselves */ + if (strcaseeq16(entry->loader, loaded_image_path)) { + entry->type = LOADER_UNDEFINED; + break; + } + + } else if (streq8(key, "architecture")) { + /* do not add an entry for an EFI image of architecture not matching with that of the image */ + if (!streq8(value, EFI_MACHINE_TYPE_NAME)) { + entry->type = LOADER_UNDEFINED; + break; + } + + } else if (streq8(key, "devicetree")) { + free(entry->devicetree); + entry->devicetree = xstr8_to_path(value); + + } else if (streq8(key, "initrd")) { + entry->initrd = xrealloc( + entry->initrd, + n_initrd == 0 ? 0 : (n_initrd + 1) * sizeof(uint16_t *), + (n_initrd + 2) * sizeof(uint16_t *)); + entry->initrd[n_initrd++] = xstr8_to_path(value); + entry->initrd[n_initrd] = NULL; + + } else if (streq8(key, "options")) { + _cleanup_free_ char16_t *new = NULL; + + new = xstr8_to_16(value); + if (entry->options) { + char16_t *s = xasprintf("%ls %ls", entry->options, new); + free(entry->options); + entry->options = s; + } else + entry->options = TAKE_PTR(new); + } + + if (entry->type == LOADER_UNDEFINED) + return; + + /* check existence */ + _cleanup_(file_closep) EFI_FILE *handle = NULL; + err = root_dir->Open(root_dir, &handle, entry->loader, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return; + + entry->device = device; + entry->id = xstrdup16(file); + strtolower16(entry->id); + + config_add_entry(config, entry); + + boot_entry_parse_tries(entry, path, file, u".conf"); + TAKE_PTR(entry); +} + +static EFI_STATUS efivar_get_timeout(const char16_t *var, uint64_t *ret_value) { + _cleanup_free_ char16_t *value = NULL; + EFI_STATUS err; + + assert(var); + assert(ret_value); + + err = efivar_get(MAKE_GUID_PTR(LOADER), var, &value); + if (err != EFI_SUCCESS) + return err; + + if (streq16(value, u"menu-disabled")) { + *ret_value = TIMEOUT_MENU_DISABLED; + return EFI_SUCCESS; + } + if (streq16(value, u"menu-force")) { + *ret_value = TIMEOUT_MENU_FORCE; + return EFI_SUCCESS; + } + if (streq16(value, u"menu-hidden")) { + *ret_value = TIMEOUT_MENU_HIDDEN; + return EFI_SUCCESS; + } + + uint64_t timeout; + if (!parse_number16(value, &timeout, NULL)) + return EFI_INVALID_PARAMETER; + + *ret_value = MIN(timeout, TIMEOUT_TYPE_MAX); + return EFI_SUCCESS; +} + +static void config_load_defaults(Config *config, EFI_FILE *root_dir) { + _cleanup_free_ char *content = NULL; + size_t content_size, value = 0; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + assert(root_dir); + + *config = (Config) { + .editor = true, + .auto_entries = true, + .auto_firmware = true, + .secure_boot_enroll = ENROLL_IF_SAFE, + .idx_default_efivar = IDX_INVALID, + .console_mode = CONSOLE_MODE_KEEP, + .console_mode_efivar = CONSOLE_MODE_KEEP, + .timeout_sec_config = TIMEOUT_UNSET, + .timeout_sec_efivar = TIMEOUT_UNSET, + }; + + err = file_read(root_dir, u"\\loader\\loader.conf", 0, 0, &content, &content_size); + if (err == EFI_SUCCESS) { + /* First, measure. */ + err = tpm_log_tagged_event( + TPM2_PCR_BOOT_LOADER_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(content), + content_size, + LOADER_CONF_EVENT_TAG_ID, + u"loader.conf", + /* ret_measured= */ NULL); + if (err != EFI_SUCCESS) + log_error_status(err, "Error measuring loader.conf into TPM: %m"); + + /* Then: parse */ + config_defaults_load_from_file(config, content); + } + + err = efivar_get_timeout(u"LoaderConfigTimeout", &config->timeout_sec_efivar); + if (err == EFI_SUCCESS) + config->timeout_sec = config->timeout_sec_efivar; + else if (err != EFI_NOT_FOUND) + log_error_status(err, "Error reading LoaderConfigTimeout EFI variable: %m"); + + err = efivar_get_timeout(u"LoaderConfigTimeoutOneShot", &config->timeout_sec); + if (err == EFI_SUCCESS) { + /* Unset variable now, after all it's "one shot". */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderConfigTimeoutOneShot", EFI_VARIABLE_NON_VOLATILE); + + config->force_menu = true; /* force the menu when this is set */ + } else if (err != EFI_NOT_FOUND) + log_error_status(err, "Error reading LoaderConfigTimeoutOneShot EFI variable: %m"); + + err = efivar_get_uint_string(MAKE_GUID_PTR(LOADER), u"LoaderConfigConsoleMode", &value); + if (err == EFI_SUCCESS) + config->console_mode_efivar = value; + + err = efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderEntryOneShot", &config->entry_oneshot); + if (err == EFI_SUCCESS) + /* Unset variable now, after all it's "one shot". */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderEntryOneShot", EFI_VARIABLE_NON_VOLATILE); + + (void) efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderEntryDefault", &config->entry_default_efivar); + + strtolower16(config->entry_default_config); + strtolower16(config->entry_default_efivar); + strtolower16(config->entry_oneshot); + strtolower16(config->entry_saved); + + config->use_saved_entry = streq16(config->entry_default_config, u"@saved"); + config->use_saved_entry_efivar = streq16(config->entry_default_efivar, u"@saved"); + if (config->use_saved_entry || config->use_saved_entry_efivar) + (void) efivar_get(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", &config->entry_saved); +} + +static void config_load_type1_entries( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir, + const char16_t *loaded_image_path) { + + _cleanup_(file_closep) EFI_FILE *entries_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *f = NULL; + size_t f_size = 0; + EFI_STATUS err; + + assert(config); + assert(device); + assert(root_dir); + + /* Adds Boot Loader Type #1 entries (i.e. /loader/entries/….conf) */ + + err = open_directory(root_dir, u"\\loader\\entries", &entries_dir); + if (err != EFI_SUCCESS) + return; + + for (;;) { + _cleanup_free_ char *content = NULL; + + err = readdir(entries_dir, &f, &f_size); + if (err != EFI_SUCCESS || !f) + break; + + if (f->FileName[0] == '.') + continue; + if (FLAGS_SET(f->Attribute, EFI_FILE_DIRECTORY)) + continue; + + if (!endswith_no_case(f->FileName, u".conf")) + continue; + if (startswith(f->FileName, u"auto-")) + continue; + + err = file_read(entries_dir, f->FileName, 0, 0, &content, NULL); + if (err == EFI_SUCCESS) + boot_entry_add_type1(config, device, root_dir, u"\\loader\\entries", f->FileName, content, loaded_image_path); + } +} + +static int boot_entry_compare(const BootEntry *a, const BootEntry *b) { + int r; + + assert(a); + assert(b); + + /* Order entries that have no tries left to the end of the list */ + r = CMP(a->tries_left == 0, b->tries_left == 0); + if (r != 0) + return r; + + /* If there's a sort key defined for *both* entries, then we do new-style ordering, i.e. by + * sort-key/machine-id/version, with a final fallback to id. If there's no sort key for either, we do + * old-style ordering, i.e. by id only. If one has sort key and the other does not, we put new-style + * before old-style. */ + r = CMP(!a->sort_key, !b->sort_key); + if (r != 0) /* one is old-style, one new-style */ + return r; + + if (a->sort_key && b->sort_key) { + r = strcmp16(a->sort_key, b->sort_key); + if (r != 0) + return r; + + /* If multiple installations of the same OS are around, group by machine ID */ + r = strcmp16(a->machine_id, b->machine_id); + if (r != 0) + return r; + + /* If the sort key was defined, then order by version now (downwards, putting the newest first) */ + r = -strverscmp_improved(a->version, b->version); + if (r != 0) + return r; + } + + /* Now order by ID. The version is likely part of the ID, thus note that this will generatelly put + * the newer versions earlier. Specifying a sort key explicitly is preferable, because it gives an + * explicit sort order. */ + r = -strverscmp_improved(a->id, b->id); + if (r != 0) + return r; + + if (a->tries_left < 0 || b->tries_left < 0) + return 0; + + /* If both items have boot counting, and otherwise are identical, put the entry with more tries left first */ + r = -CMP(a->tries_left, b->tries_left); + if (r != 0) + return r; + + /* If they have the same number of tries left, then let the one win which was tried fewer times so far */ + return CMP(a->tries_done, b->tries_done); +} + +static size_t config_find_entry(Config *config, const char16_t *pattern) { + assert(config); + + /* We expect pattern and entry IDs to be already case folded. */ + + if (!pattern) + return IDX_INVALID; + + for (size_t i = 0; i < config->n_entries; i++) + if (efi_fnmatch(pattern, config->entries[i]->id)) + return i; + + return IDX_INVALID; +} + +static void config_select_default_entry(Config *config) { + size_t i; + + assert(config); + + i = config_find_entry(config, config->entry_oneshot); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + i = config_find_entry(config, config->use_saved_entry_efivar ? config->entry_saved : config->entry_default_efivar); + if (i != IDX_INVALID) { + config->idx_default = i; + config->idx_default_efivar = i; + return; + } + + if (config->use_saved_entry) + /* No need to do the same thing twice. */ + i = config->use_saved_entry_efivar ? IDX_INVALID : config_find_entry(config, config->entry_saved); + else + i = config_find_entry(config, config->entry_default_config); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + /* select the first suitable entry */ + for (i = 0; i < config->n_entries; i++) + if (config->entries[i]->type != LOADER_AUTO && !config->entries[i]->call) { + config->idx_default = i; + return; + } + + /* If no configured entry to select from was found, enable the menu. */ + config->idx_default = 0; + if (config->timeout_sec == 0) + config->timeout_sec = 10; +} + +static bool entries_unique(BootEntry **entries, bool *unique, size_t n_entries) { + bool is_unique = true; + + assert(entries); + assert(unique); + + for (size_t i = 0; i < n_entries; i++) + for (size_t k = i + 1; k < n_entries; k++) { + if (!streq16(entries[i]->title_show, entries[k]->title_show)) + continue; + + is_unique = unique[i] = unique[k] = false; + } + + return is_unique; +} + +/* generate unique titles, avoiding non-distinguishable menu entries */ +static void generate_boot_entry_titles(Config *config) { + assert(config); + + bool unique[config->n_entries]; + + /* set title */ + for (size_t i = 0; i < config->n_entries; i++) { + assert(!config->entries[i]->title_show); + unique[i] = true; + config->entries[i]->title_show = xstrdup16(config->entries[i]->title ?: config->entries[i]->id); + } + + if (entries_unique(config->entries, unique, config->n_entries)) + return; + + /* add version to non-unique titles */ + for (size_t i = 0; i < config->n_entries; i++) { + if (unique[i]) + continue; + + unique[i] = true; + + if (!config->entries[i]->version) + continue; + + _cleanup_free_ char16_t *t = config->entries[i]->title_show; + config->entries[i]->title_show = xasprintf("%ls (%ls)", t, config->entries[i]->version); + } + + if (entries_unique(config->entries, unique, config->n_entries)) + return; + + /* add machine-id to non-unique titles */ + for (size_t i = 0; i < config->n_entries; i++) { + if (unique[i]) + continue; + + unique[i] = true; + + if (!config->entries[i]->machine_id) + continue; + + _cleanup_free_ char16_t *t = config->entries[i]->title_show; + config->entries[i]->title_show = xasprintf("%ls (%.8ls)", t, config->entries[i]->machine_id); + } + + if (entries_unique(config->entries, unique, config->n_entries)) + return; + + /* add file name to non-unique titles */ + for (size_t i = 0; i < config->n_entries; i++) { + if (unique[i]) + continue; + + _cleanup_free_ char16_t *t = config->entries[i]->title_show; + config->entries[i]->title_show = xasprintf("%ls (%ls)", t, config->entries[i]->id); + } +} + +static bool is_sd_boot(EFI_FILE *root_dir, const char16_t *loader_path) { + EFI_STATUS err; + static const char * const sections[] = { + ".sdmagic", + NULL + }; + size_t offset = 0, size = 0, read; + _cleanup_free_ char *content = NULL; + + assert(root_dir); + assert(loader_path); + + err = pe_file_locate_sections(root_dir, loader_path, sections, &offset, &size); + if (err != EFI_SUCCESS || size != sizeof(SD_MAGIC)) + return false; + + err = file_read(root_dir, loader_path, offset, size, &content, &read); + if (err != EFI_SUCCESS || size != read) + return false; + + return memcmp(content, SD_MAGIC, sizeof(SD_MAGIC)) == 0; +} + +static BootEntry* config_add_entry_loader_auto( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir, + const char16_t *loaded_image_path, + const char16_t *id, + char16_t key, + const char16_t *title, + const char16_t *loader) { + + assert(config); + assert(device); + assert(root_dir); + assert(id); + assert(title); + + if (!config->auto_entries) + return NULL; + + if (!loader) { + loader = u"\\EFI\\BOOT\\BOOT" EFI_MACHINE_TYPE_NAME ".efi"; + + /* We are trying to add the default EFI loader here, + * but we do not want to do that if that would be us. + * + * If the default loader is not us, it might be shim. It would + * chainload GRUBX64.EFI in that case, which might be us. */ + if (strcaseeq16(loader, loaded_image_path) || + is_sd_boot(root_dir, loader) || + is_sd_boot(root_dir, u"\\EFI\\BOOT\\GRUB" EFI_MACHINE_TYPE_NAME u".EFI")) + return NULL; + } + + /* check existence */ + _cleanup_(file_closep) EFI_FILE *handle = NULL; + EFI_STATUS err = root_dir->Open(root_dir, &handle, (char16_t *) loader, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return NULL; + + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(id), + .type = LOADER_AUTO, + .title = xstrdup16(title), + .device = device, + .loader = xstrdup16(loader), + .key = key, + .tries_done = -1, + .tries_left = -1, + }; + + config_add_entry(config, entry); + return entry; +} + +static void config_add_entry_osx(Config *config) { + EFI_STATUS err; + size_t n_handles = 0; + _cleanup_free_ EFI_HANDLE *handles = NULL; + + assert(config); + + if (!config->auto_entries) + return; + + err = BS->LocateHandleBuffer( + ByProtocol, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return; + + for (size_t i = 0; i < n_handles; i++) { + _cleanup_(file_closep) EFI_FILE *root = NULL; + + if (open_volume(handles[i], &root) != EFI_SUCCESS) + continue; + + if (config_add_entry_loader_auto( + config, + handles[i], + root, + NULL, + u"auto-osx", + 'a', + u"macOS", + u"\\System\\Library\\CoreServices\\boot.efi")) + break; + } +} + +#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) +static EFI_STATUS boot_windows_bitlocker(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles; + EFI_STATUS err; + + // FIXME: Experimental for now. Should be generalized, and become a per-entry option that can be + // enabled independently of BitLocker, and without a BootXXXX entry pre-existing. + + /* BitLocker key cannot be sealed without a TPM present. */ + if (!tpm_present()) + return EFI_NOT_FOUND; + + err = BS->LocateHandleBuffer( + ByProtocol, MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return err; + + /* Look for BitLocker magic string on all block drives. */ + bool found = false; + for (size_t i = 0; i < n_handles; i++) { + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->HandleProtocol(handles[i], MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), (void **) &block_io); + if (err != EFI_SUCCESS || block_io->Media->BlockSize < 512 || block_io->Media->BlockSize > 4096) + continue; + + char buf[4096]; + err = block_io->ReadBlocks(block_io, block_io->Media->MediaId, 0, sizeof(buf), buf); + if (err != EFI_SUCCESS) + continue; + + if (memcmp(buf + 3, "-FVE-FS-", STRLEN("-FVE-FS-")) == 0) { + found = true; + break; + } + } + + /* If no BitLocker drive was found, we can just chainload bootmgfw.efi directly. */ + if (!found) + return EFI_NOT_FOUND; + + _cleanup_free_ uint16_t *boot_order = NULL; + size_t boot_order_size; + + /* There can be gaps in Boot#### entries. Instead of iterating over the full + * EFI var list or uint16_t namespace, just look for "Windows Boot Manager" in BootOrder. */ + err = efivar_get_raw(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"BootOrder", (char **) &boot_order, &boot_order_size); + if (err != EFI_SUCCESS || boot_order_size % sizeof(uint16_t) != 0) + return err; + + for (size_t i = 0; i < boot_order_size / sizeof(uint16_t); i++) { + _cleanup_free_ char *buf = NULL; + size_t buf_size; + + _cleanup_free_ char16_t *name = xasprintf("Boot%04x", boot_order[i]); + err = efivar_get_raw(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), name, &buf, &buf_size); + if (err != EFI_SUCCESS) + continue; + + /* Boot#### are EFI_LOAD_OPTION. But we really are only interested + * for the description, which is at this offset. */ + size_t offset = sizeof(uint32_t) + sizeof(uint16_t); + if (buf_size < offset + sizeof(char16_t)) + continue; + + if (streq16((char16_t *) (buf + offset), u"Windows Boot Manager")) { + err = efivar_set_raw( + MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), + u"BootNext", + boot_order + i, + sizeof(boot_order[i]), + EFI_VARIABLE_NON_VOLATILE); + if (err != EFI_SUCCESS) + return err; + RT->ResetSystem(EfiResetWarm, EFI_SUCCESS, 0, NULL); + assert_not_reached(); + } + } + + return EFI_NOT_FOUND; +} +#endif + +static void config_add_entry_windows(Config *config, EFI_HANDLE *device, EFI_FILE *root_dir) { +#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) + _cleanup_free_ char *bcd = NULL; + char16_t *title = NULL; + EFI_STATUS err; + size_t len; + + assert(config); + assert(device); + assert(root_dir); + + if (!config->auto_entries) + return; + + /* Try to find a better title. */ + err = file_read(root_dir, u"\\EFI\\Microsoft\\Boot\\BCD", 0, 100*1024, &bcd, &len); + if (err == EFI_SUCCESS) + title = get_bcd_title((uint8_t *) bcd, len); + + BootEntry *e = config_add_entry_loader_auto(config, device, root_dir, NULL, + u"auto-windows", 'w', title ?: u"Windows Boot Manager", + u"\\EFI\\Microsoft\\Boot\\bootmgfw.efi"); + + if (config->reboot_for_bitlocker) + e->call = boot_windows_bitlocker; +#endif +} + +static void config_load_type2_entries( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir) { + + _cleanup_(file_closep) EFI_FILE *linux_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *f = NULL; + size_t f_size = 0; + EFI_STATUS err; + + /* Adds Boot Loader Type #2 entries (i.e. /EFI/Linux/….efi) */ + + assert(config); + assert(device); + assert(root_dir); + + err = open_directory(root_dir, u"\\EFI\\Linux", &linux_dir); + if (err != EFI_SUCCESS) + return; + + for (;;) { + enum { + SECTION_CMDLINE, + SECTION_OSREL, + _SECTION_MAX, + }; + + static const char * const sections[_SECTION_MAX + 1] = { + [SECTION_CMDLINE] = ".cmdline", + [SECTION_OSREL] = ".osrel", + NULL, + }; + + _cleanup_free_ char16_t *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL, + *os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL; + const char16_t *good_name, *good_version, *good_sort_key; + _cleanup_free_ char *content = NULL; + size_t offs[_SECTION_MAX] = {}, szs[_SECTION_MAX] = {}, pos = 0; + char *line, *key, *value; + + err = readdir(linux_dir, &f, &f_size); + if (err != EFI_SUCCESS || !f) + break; + + if (f->FileName[0] == '.') + continue; + if (FLAGS_SET(f->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (!endswith_no_case(f->FileName, u".efi")) + continue; + if (startswith(f->FileName, u"auto-")) + continue; + + /* look for .osrel and .cmdline sections in the .efi binary */ + err = pe_file_locate_sections(linux_dir, f->FileName, sections, offs, szs); + if (err != EFI_SUCCESS || szs[SECTION_OSREL] == 0) + continue; + + err = file_read(linux_dir, f->FileName, offs[SECTION_OSREL], szs[SECTION_OSREL], &content, NULL); + if (err != EFI_SUCCESS) + continue; + + /* read properties from the embedded os-release file */ + while ((line = line_get_key_value(content, "=", &pos, &key, &value))) + if (streq8(key, "PRETTY_NAME")) { + free(os_pretty_name); + os_pretty_name = xstr8_to_16(value); + + } else if (streq8(key, "IMAGE_ID")) { + free(os_image_id); + os_image_id = xstr8_to_16(value); + + } else if (streq8(key, "NAME")) { + free(os_name); + os_name = xstr8_to_16(value); + + } else if (streq8(key, "ID")) { + free(os_id); + os_id = xstr8_to_16(value); + + } else if (streq8(key, "IMAGE_VERSION")) { + free(os_image_version); + os_image_version = xstr8_to_16(value); + + } else if (streq8(key, "VERSION")) { + free(os_version); + os_version = xstr8_to_16(value); + + } else if (streq8(key, "VERSION_ID")) { + free(os_version_id); + os_version_id = xstr8_to_16(value); + + } else if (streq8(key, "BUILD_ID")) { + free(os_build_id); + os_build_id = xstr8_to_16(value); + } + + if (!bootspec_pick_name_version_sort_key( + os_pretty_name, + os_image_id, + os_name, + os_id, + os_image_version, + os_version, + os_version_id, + os_build_id, + &good_name, + &good_version, + &good_sort_key)) + continue; + + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(f->FileName), + .type = LOADER_UNIFIED_LINUX, + .title = xstrdup16(good_name), + .version = xstrdup16(good_version), + .device = device, + .loader = xasprintf("\\EFI\\Linux\\%ls", f->FileName), + .sort_key = xstrdup16(good_sort_key), + .key = 'l', + .tries_done = -1, + .tries_left = -1, + }; + + strtolower16(entry->id); + config_add_entry(config, entry); + boot_entry_parse_tries(entry, u"\\EFI\\Linux", f->FileName, u".efi"); + + if (szs[SECTION_CMDLINE] == 0) + continue; + + content = mfree(content); + + /* read the embedded cmdline file */ + size_t cmdline_len; + err = file_read(linux_dir, f->FileName, offs[SECTION_CMDLINE], szs[SECTION_CMDLINE], &content, &cmdline_len); + if (err == EFI_SUCCESS) { + entry->options = xstrn8_to_16(content, cmdline_len); + mangle_stub_cmdline(entry->options); + entry->options_implied = true; + } + } +} + +static void config_load_xbootldr( + Config *config, + EFI_HANDLE *device) { + + _cleanup_(file_closep) EFI_FILE *root_dir = NULL; + EFI_HANDLE new_device = NULL; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + assert(config); + assert(device); + + err = partition_open(MAKE_GUID_PTR(XBOOTLDR), device, &new_device, &root_dir); + if (err != EFI_SUCCESS) + return; + + config_load_type2_entries(config, new_device, root_dir); + config_load_type1_entries(config, new_device, root_dir, NULL); +} + +static EFI_STATUS initrd_prepare( + EFI_FILE *root, + const BootEntry *entry, + char16_t **ret_options, + void **ret_initrd, + size_t *ret_initrd_size) { + + assert(root); + assert(entry); + assert(ret_options); + assert(ret_initrd); + assert(ret_initrd_size); + + if (entry->type != LOADER_LINUX || !entry->initrd) { + ret_options = NULL; + ret_initrd = NULL; + ret_initrd_size = 0; + return EFI_SUCCESS; + } + + /* Note that order of initrds matters. The kernel will only look for microcode updates in the very + * first one it sees. */ + + /* Add initrd= to options for older kernels that do not support LINUX_INITRD_MEDIA. Should be dropped + * if linux_x86.c is dropped. */ + _cleanup_free_ char16_t *options = NULL; + + EFI_STATUS err; + size_t size = 0; + _cleanup_free_ uint8_t *initrd = NULL; + + STRV_FOREACH(i, entry->initrd) { + _cleanup_free_ char16_t *o = options; + if (o) + options = xasprintf("%ls initrd=%ls", o, *i); + else + options = xasprintf("initrd=%ls", *i); + + _cleanup_(file_closep) EFI_FILE *handle = NULL; + err = root->Open(root, &handle, *i, EFI_FILE_MODE_READ, 0); + if (err != EFI_SUCCESS) + return err; + + _cleanup_free_ EFI_FILE_INFO *info = NULL; + err = get_file_info(handle, &info, NULL); + if (err != EFI_SUCCESS) + return err; + + if (info->FileSize == 0) /* Automatically skip over empty files */ + continue; + + size_t new_size, read_size = info->FileSize; + if (__builtin_add_overflow(size, read_size, &new_size)) + return EFI_OUT_OF_RESOURCES; + initrd = xrealloc(initrd, size, new_size); + + err = chunked_read(handle, &read_size, initrd + size); + if (err != EFI_SUCCESS) + return err; + + /* Make sure the actual read size is what we expected. */ + assert(size + read_size == new_size); + size = new_size; + } + + if (entry->options) { + _cleanup_free_ char16_t *o = options; + options = xasprintf("%ls %ls", o, entry->options); + } + + *ret_options = TAKE_PTR(options); + *ret_initrd = TAKE_PTR(initrd); + *ret_initrd_size = size; + return EFI_SUCCESS; +} + +static EFI_STATUS image_start( + EFI_HANDLE parent_image, + const BootEntry *entry) { + + _cleanup_(devicetree_cleanup) struct devicetree_state dtstate = {}; + _cleanup_(unload_imagep) EFI_HANDLE image = NULL; + _cleanup_free_ EFI_DEVICE_PATH *path = NULL; + EFI_STATUS err; + + assert(entry); + + /* If this loader entry has a special way to boot, try that first. */ + if (entry->call) + (void) entry->call(); + + _cleanup_(file_closep) EFI_FILE *image_root = NULL; + err = open_volume(entry->device, &image_root); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error opening root path: %m"); + + err = make_file_device_path(entry->device, entry->loader, &path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making file device path: %m"); + + size_t initrd_size = 0; + _cleanup_free_ void *initrd = NULL; + _cleanup_free_ char16_t *options_initrd = NULL; + err = initrd_prepare(image_root, entry, &options_initrd, &initrd, &initrd_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error preparing initrd: %m"); + + err = shim_load_image(parent_image, path, &image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error loading %ls: %m", entry->loader); + + /* DTBs are loaded by the kernel before ExitBootServices, and they can be used to map and assign + * arbitrary memory ranges, so skip them when secure boot is enabled as the DTB here is unverified. + */ + if (entry->devicetree && !secure_boot_enabled()) { + err = devicetree_install(&dtstate, image_root, entry->devicetree); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error loading %ls: %m", entry->devicetree); + } + + _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; + err = initrd_register(initrd, initrd_size, &initrd_handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error registering initrd: %m"); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting LoadedImageProtocol handle: %m"); + + /* If we had to append an initrd= entry to the command line, we have to pass it, and measure it. + * Otherwise, only pass/measure it if it is not implicit anyway (i.e. embedded into the UKI or + * so). */ + char16_t *options = options_initrd ?: entry->options_implied ? NULL : entry->options; + if (options) { + loaded_image->LoadOptions = options; + loaded_image->LoadOptionsSize = strsize16(options); + + /* Try to log any options to the TPM, especially to catch manually edited options */ + (void) tpm_log_load_options(options, NULL); + } + + efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeExecUSec", 0); + err = BS->StartImage(image, NULL, NULL); + graphics_mode(false); + if (err == EFI_SUCCESS) + return EFI_SUCCESS; + + /* Try calling the kernel compat entry point if one exists. */ + if (err == EFI_UNSUPPORTED && entry->type == LOADER_LINUX) { + uint32_t compat_address; + + err = pe_kernel_info(loaded_image->ImageBase, &compat_address); + if (err != EFI_SUCCESS) { + if (err != EFI_UNSUPPORTED) + return log_error_status(err, "Error finding kernel compat entry address: %m"); + } else if (compat_address > 0) { + EFI_IMAGE_ENTRY_POINT kernel_entry = + (EFI_IMAGE_ENTRY_POINT) ((uint8_t *) loaded_image->ImageBase + compat_address); + + err = kernel_entry(image, ST); + graphics_mode(false); + if (err == EFI_SUCCESS) + return EFI_SUCCESS; + } else + err = EFI_UNSUPPORTED; + } + + return log_error_status(err, "Failed to execute %ls (%ls): %m", entry->title_show, entry->loader); +} + +static void config_free(Config *config) { + assert(config); + for (size_t i = 0; i < config->n_entries; i++) + boot_entry_free(config->entries[i]); + free(config->entries); + free(config->entry_default_config); + free(config->entry_default_efivar); + free(config->entry_oneshot); + free(config->entry_saved); +} + +static void config_write_entries_to_variable(Config *config) { + _cleanup_free_ char *buffer = NULL; + size_t sz = 0; + char *p; + + assert(config); + + for (size_t i = 0; i < config->n_entries; i++) + sz += strsize16(config->entries[i]->id); + + p = buffer = xmalloc(sz); + + for (size_t i = 0; i < config->n_entries; i++) + p = mempcpy(p, config->entries[i]->id, strsize16(config->entries[i]->id)); + + assert(p == buffer + sz); + + /* Store the full list of discovered entries. */ + (void) efivar_set_raw(MAKE_GUID_PTR(LOADER), u"LoaderEntries", buffer, sz, 0); +} + +static void save_selected_entry(const Config *config, const BootEntry *entry) { + assert(config); + assert(entry); + assert(entry->loader || !entry->call); + + /* Always export the selected boot entry to the system in a volatile var. */ + (void) efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderEntrySelected", entry->id, 0); + + /* Do not save or delete if this was a oneshot boot. */ + if (streq16(config->entry_oneshot, entry->id)) + return; + + if (config->use_saved_entry_efivar || (!config->entry_default_efivar && config->use_saved_entry)) { + /* Avoid unnecessary NVRAM writes. */ + if (streq16(config->entry_saved, entry->id)) + return; + + (void) efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", entry->id, EFI_VARIABLE_NON_VOLATILE); + } else + /* Delete the non-volatile var if not needed. */ + (void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", EFI_VARIABLE_NON_VOLATILE); +} + +static EFI_STATUS secure_boot_discover_keys(Config *config, EFI_FILE *root_dir) { + EFI_STATUS err; + _cleanup_(file_closep) EFI_FILE *keys_basedir = NULL; + + if (secure_boot_mode() != SECURE_BOOT_SETUP) + return EFI_SUCCESS; + + /* the lack of a 'keys' directory is not fatal and is silently ignored */ + err = open_directory(root_dir, u"\\loader\\keys", &keys_basedir); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return err; + + for (;;) { + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + size_t dirent_size = 0; + BootEntry *entry = NULL; + + err = readdir(keys_basedir, &dirent, &dirent_size); + if (err != EFI_SUCCESS || !dirent) + return err; + + if (dirent->FileName[0] == '.') + continue; + + if (!FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + + entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xasprintf("secure-boot-keys-%ls", dirent->FileName), + .title = xasprintf("Enroll Secure Boot keys: %ls", dirent->FileName), + .path = xasprintf("\\loader\\keys\\%ls", dirent->FileName), + .type = LOADER_SECURE_BOOT_KEYS, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + + if (IN_SET(config->secure_boot_enroll, ENROLL_IF_SAFE, ENROLL_FORCE) && + strcaseeq16(dirent->FileName, u"auto")) + /* If we auto enroll successfully this call does not return. + * If it fails we still want to add other potential entries to the menu. */ + secure_boot_enroll_at(root_dir, entry->path, config->secure_boot_enroll == ENROLL_FORCE); + } + + return EFI_SUCCESS; +} + +static void export_variables( + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *loaded_image_path, + uint64_t init_usec) { + + static const uint64_t loader_features = + EFI_LOADER_FEATURE_CONFIG_TIMEOUT | + EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT | + EFI_LOADER_FEATURE_ENTRY_DEFAULT | + EFI_LOADER_FEATURE_ENTRY_ONESHOT | + EFI_LOADER_FEATURE_BOOT_COUNTING | + EFI_LOADER_FEATURE_XBOOTLDR | + EFI_LOADER_FEATURE_RANDOM_SEED | + EFI_LOADER_FEATURE_LOAD_DRIVER | + EFI_LOADER_FEATURE_SORT_KEY | + EFI_LOADER_FEATURE_SAVED_ENTRY | + EFI_LOADER_FEATURE_DEVICETREE | + EFI_LOADER_FEATURE_SECUREBOOT_ENROLL | + EFI_LOADER_FEATURE_RETAIN_SHIM | + EFI_LOADER_FEATURE_MENU_DISABLE | + 0; + + _cleanup_free_ char16_t *infostr = NULL, *typestr = NULL; + + assert(loaded_image); + + efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeInitUSec", init_usec); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderInfo", u"systemd-boot " GIT_VERSION, 0); + + infostr = xasprintf("%ls %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", infostr, 0); + + typestr = xasprintf("UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareType", typestr, 0); + + (void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"LoaderFeatures", loader_features, 0); + + /* the filesystem path to this image, to prevent adding ourselves to the menu */ + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderImageIdentifier", loaded_image_path, 0); + + /* export the device path this image is started from */ + _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); + if (uuid) + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", uuid, 0); +} + +static void config_load_all_entries( + Config *config, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *loaded_image_path, + EFI_FILE *root_dir) { + + assert(config); + assert(loaded_image); + assert(root_dir); + + config_load_defaults(config, root_dir); + + /* Scan /EFI/Linux/ directory */ + config_load_type2_entries(config, loaded_image->DeviceHandle, root_dir); + + /* Scan /loader/entries/\*.conf files */ + config_load_type1_entries(config, loaded_image->DeviceHandle, root_dir, loaded_image_path); + + /* Similar, but on any XBOOTLDR partition */ + config_load_xbootldr(config, loaded_image->DeviceHandle); + + /* Sort entries after version number */ + sort_pointer_array((void **) config->entries, config->n_entries, (compare_pointer_func_t) boot_entry_compare); + + /* If we find some well-known loaders, add them to the end of the list */ + config_add_entry_osx(config); + config_add_entry_windows(config, loaded_image->DeviceHandle, root_dir); + config_add_entry_loader_auto(config, loaded_image->DeviceHandle, root_dir, NULL, + u"auto-efi-shell", 's', u"EFI Shell", u"\\shell" EFI_MACHINE_TYPE_NAME ".efi"); + config_add_entry_loader_auto(config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + u"auto-efi-default", '\0', u"EFI Default Loader", NULL); + + if (config->auto_firmware && FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(u"auto-reboot-to-firmware-setup"), + .title = xstrdup16(u"Reboot Into Firmware Interface"), + .call = reboot_into_firmware, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + } + + if (config->auto_poweroff) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(u"auto-poweroff"), + .title = xstrdup16(u"Power Off The System"), + .call = poweroff_system, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + } + + if (config->auto_reboot) { + BootEntry *entry = xnew(BootEntry, 1); + *entry = (BootEntry) { + .id = xstrdup16(u"auto-reboot"), + .title = xstrdup16(u"Reboot The System"), + .call = reboot_system, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + } + + /* Find secure boot signing keys and autoload them if configured. + * Otherwise, create menu entries so that the user can load them manually. + * If the secure-boot-enroll variable is set to no (the default), we do not + * even search for keys on the ESP */ + if (config->secure_boot_enroll != ENROLL_OFF) + secure_boot_discover_keys(config, root_dir); + + if (config->n_entries == 0) + return; + + config_write_entries_to_variable(config); + + generate_boot_entry_titles(config); + + /* Select entry by configured pattern or EFI LoaderDefaultEntry= variable */ + config_select_default_entry(config); +} + +static EFI_STATUS discover_root_dir(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, EFI_FILE **ret_dir) { + if (is_direct_boot(loaded_image->DeviceHandle)) + return vmm_open(&loaded_image->DeviceHandle, ret_dir); + else + return open_volume(loaded_image->DeviceHandle, ret_dir); +} + +static EFI_STATUS run(EFI_HANDLE image) { + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + _cleanup_(file_closep) EFI_FILE *root_dir = NULL; + _cleanup_(config_free) Config config = {}; + _cleanup_free_ char16_t *loaded_image_path = NULL; + EFI_STATUS err; + uint64_t init_usec; + bool menu = false; + + init_usec = time_usec(); + + /* Ask Shim to leave its protocol around, so that the stub can use it to validate PEs. + * By default, Shim uninstalls its protocol when calling StartImage(). */ + shim_retain_protocol(); + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); + + (void) device_path_to_str(loaded_image->FilePath, &loaded_image_path); + + export_variables(loaded_image, loaded_image_path, init_usec); + + err = discover_root_dir(loaded_image, &root_dir); + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + (void) load_drivers(image, loaded_image, root_dir); + + config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir); + + if (config.n_entries == 0) + return log_error_status( + EFI_NOT_FOUND, + "No loader found. Configuration files in \\loader\\entries\\*.conf are needed."); + + /* select entry or show menu when key is pressed or timeout is set */ + if (config.force_menu || !IN_SET(config.timeout_sec, TIMEOUT_MENU_HIDDEN, TIMEOUT_MENU_DISABLED)) + menu = true; + else if (config.timeout_sec != TIMEOUT_MENU_DISABLED) { + uint64_t key; + + /* Block up to 100ms to give firmware time to get input working. */ + err = console_key_read(&key, 100 * 1000); + if (err == EFI_SUCCESS) { + /* find matching key in boot entries */ + size_t idx = entry_lookup_key(&config, config.idx_default, KEYCHAR(key)); + if (idx != IDX_INVALID) + config.idx_default = idx; + else + menu = true; + } + } + + for (;;) { + BootEntry *entry; + + entry = config.entries[config.idx_default]; + if (menu) { + efivar_set_time_usec(MAKE_GUID_PTR(LOADER), u"LoaderTimeMenuUSec", 0); + if (!menu_run(&config, &entry, loaded_image_path)) + return EFI_SUCCESS; + } + + /* if auto enrollment is activated, we try to load keys for the given entry. */ + if (entry->type == LOADER_SECURE_BOOT_KEYS && config.secure_boot_enroll != ENROLL_OFF) { + err = secure_boot_enroll_at(root_dir, entry->path, /*force=*/ true); + if (err != EFI_SUCCESS) + return err; + continue; + } + + /* Run special entry like "reboot" now. Those that have a loader + * will be handled by image_start() instead. */ + if (entry->call && !entry->loader) { + entry->call(); + continue; + } + + (void) boot_entry_bump_counters(entry); + save_selected_entry(&config, entry); + + /* Optionally, read a random seed off the ESP and pass it to the OS */ + (void) process_random_seed(root_dir); + + err = image_start(image, entry); + if (err != EFI_SUCCESS) + return err; + + menu = true; + config.timeout_sec = 0; + } +} + +DEFINE_EFI_MAIN_FUNCTION(run, "systemd-boot", /*wait_for_debugger=*/false); diff --git a/src/boot/efi/console.c b/src/boot/efi/console.c new file mode 100644 index 0000000..067ee7c --- /dev/null +++ b/src/boot/efi/console.c @@ -0,0 +1,312 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "console.h" +#include "proto/graphics-output.h" +#include "util.h" + +#define SYSTEM_FONT_WIDTH 8 +#define SYSTEM_FONT_HEIGHT 19 +#define HORIZONTAL_MAX_OK 1920 +#define VERTICAL_MAX_OK 1080 +#define VIEWPORT_RATIO 10 + +static void event_closep(EFI_EVENT *event) { + if (!*event) + return; + + BS->CloseEvent(*event); +} + +/* + * Reading input from the console sounds like an easy task to do, but thanks to broken + * firmware it is actually a nightmare. + * + * There is a SimpleTextInput and SimpleTextInputEx API for this. Ideally we want to use + * TextInputEx, because that gives us Ctrl/Alt/Shift key state information. Unfortunately, + * it is not always available and sometimes just non-functional. + * + * On some firmware, calling ReadKeyStroke or ReadKeyStrokeEx on the default console input + * device will just freeze no matter what (even though it *reported* being ready). + * Also, multiple input protocols can be backed by the same device, but they can be out of + * sync. Falling back on a different protocol can end up with double input. + * + * Therefore, we will preferably use TextInputEx for ConIn if that is available. Additionally, + * we look for the first TextInputEx device the firmware gives us as a fallback option. It + * will replace ConInEx permanently if it ever reports a key press. + * Lastly, a timer event allows us to provide a input timeout without having to call into + * any input functions that can freeze on us or using a busy/stall loop. */ +EFI_STATUS console_key_read(uint64_t *key, uint64_t timeout_usec) { + static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *conInEx = NULL, *extraInEx = NULL; + static bool checked = false; + size_t index; + EFI_STATUS err; + _cleanup_(event_closep) EFI_EVENT timer = NULL; + + assert(key); + + if (!checked) { + /* Get the *first* TextInputEx device. */ + err = BS->LocateProtocol( + MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL), NULL, (void **) &extraInEx); + if (err != EFI_SUCCESS || BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER) + /* If WaitForKeyEx fails here, the firmware pretends it talks this + * protocol, but it really doesn't. */ + extraInEx = NULL; + + /* Get the TextInputEx version of ST->ConIn. */ + err = BS->HandleProtocol( + ST->ConsoleInHandle, + MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL), + (void **) &conInEx); + if (err != EFI_SUCCESS || BS->CheckEvent(conInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER) + conInEx = NULL; + + if (conInEx == extraInEx) + extraInEx = NULL; + + checked = true; + } + + err = BS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &timer); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error creating timer event: %m"); + + EFI_EVENT events[] = { + timer, + conInEx ? conInEx->WaitForKeyEx : ST->ConIn->WaitForKey, + extraInEx ? extraInEx->WaitForKeyEx : NULL, + }; + size_t n_events = extraInEx ? 3 : 2; + + /* Watchdog rearming loop in case the user never provides us with input or some + * broken firmware never returns from WaitForEvent. */ + for (;;) { + uint64_t watchdog_timeout_sec = 5 * 60, + watchdog_ping_usec = watchdog_timeout_sec / 2 * 1000 * 1000; + + /* SetTimer expects 100ns units for some reason. */ + err = BS->SetTimer( + timer, + TimerRelative, + MIN(timeout_usec, watchdog_ping_usec) * 10); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error arming timer event: %m"); + + (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL); + err = BS->WaitForEvent(n_events, events, &index); + (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL); + + if (err != EFI_SUCCESS) + return log_error_status(err, "Error waiting for events: %m"); + + /* We have keyboard input, process it after this loop. */ + if (timer != events[index]) + break; + + /* The EFI timer fired instead. If this was a watchdog timeout, loop again. */ + if (timeout_usec == UINT64_MAX) + continue; + else if (timeout_usec > watchdog_ping_usec) { + timeout_usec -= watchdog_ping_usec; + continue; + } + + /* The caller requested a timeout? They shall have one! */ + return EFI_TIMEOUT; + } + + /* If the extra input device we found returns something, always use that instead + * to work around broken firmware freezing on ConIn/ConInEx. */ + if (extraInEx && BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_SUCCESS) { + conInEx = extraInEx; + extraInEx = NULL; + } + + /* Do not fall back to ConIn if we have a ConIn that supports TextInputEx. + * The two may be out of sync on some firmware, giving us double input. */ + if (conInEx) { + EFI_KEY_DATA keydata; + uint32_t shift = 0; + + err = conInEx->ReadKeyStrokeEx(conInEx, &keydata); + if (err != EFI_SUCCESS) + return err; + + if (FLAGS_SET(keydata.KeyState.KeyShiftState, EFI_SHIFT_STATE_VALID)) { + /* Do not distinguish between left and right keys (set both flags). */ + if (keydata.KeyState.KeyShiftState & EFI_CONTROL_PRESSED) + shift |= EFI_CONTROL_PRESSED; + if (keydata.KeyState.KeyShiftState & EFI_ALT_PRESSED) + shift |= EFI_ALT_PRESSED; + if (keydata.KeyState.KeyShiftState & EFI_LOGO_PRESSED) + shift |= EFI_LOGO_PRESSED; + + /* Shift is not supposed to be reported for keys that can be represented as uppercase + * unicode chars (Shift+f is reported as F instead). Some firmware does it anyway, so + * filter those out. */ + if ((keydata.KeyState.KeyShiftState & EFI_SHIFT_PRESSED) && + keydata.Key.UnicodeChar == 0) + shift |= EFI_SHIFT_PRESSED; + } + + /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */ + *key = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar); + return EFI_SUCCESS; + } else if (BS->CheckEvent(ST->ConIn->WaitForKey) == EFI_SUCCESS) { + EFI_INPUT_KEY k; + + err = ST->ConIn->ReadKeyStroke(ST->ConIn, &k); + if (err != EFI_SUCCESS) + return err; + + *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar); + return EFI_SUCCESS; + } + + return EFI_NOT_READY; +} + +static EFI_STATUS change_mode(int64_t mode) { + EFI_STATUS err; + int32_t old_mode; + + /* SetMode expects a size_t, so make sure these values are sane. */ + mode = CLAMP(mode, CONSOLE_MODE_RANGE_MIN, CONSOLE_MODE_RANGE_MAX); + old_mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode); + + log_wait(); + err = ST->ConOut->SetMode(ST->ConOut, mode); + if (err == EFI_SUCCESS) + return EFI_SUCCESS; + + /* Something went wrong. Output is probably borked, so try to revert to previous mode. */ + if (ST->ConOut->SetMode(ST->ConOut, old_mode) == EFI_SUCCESS) + return err; + + /* Maybe the device is on fire? */ + ST->ConOut->Reset(ST->ConOut, true); + ST->ConOut->SetMode(ST->ConOut, CONSOLE_MODE_RANGE_MIN); + return err; +} + +EFI_STATUS query_screen_resolution(uint32_t *ret_w, uint32_t *ret_h) { + EFI_STATUS err; + EFI_GRAPHICS_OUTPUT_PROTOCOL *go; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), NULL, (void **) &go); + if (err != EFI_SUCCESS) + return err; + + if (!go->Mode || !go->Mode->Info) + return EFI_DEVICE_ERROR; + + *ret_w = go->Mode->Info->HorizontalResolution; + *ret_h = go->Mode->Info->VerticalResolution; + return EFI_SUCCESS; +} + +static int64_t get_auto_mode(void) { + uint32_t screen_width, screen_height; + + if (query_screen_resolution(&screen_width, &screen_height) == EFI_SUCCESS) { + bool keep = false; + + /* Start verifying if we are in a resolution larger than Full HD + * (1920x1080). If we're not, assume we're in a good mode and do not + * try to change it. */ + if (screen_width <= HORIZONTAL_MAX_OK && screen_height <= VERTICAL_MAX_OK) + keep = true; + /* For larger resolutions, calculate the ratio of the total screen + * area to the text viewport area. If it's less than 10 times bigger, + * then assume the text is readable and keep the text mode. */ + else { + uint64_t text_area; + size_t x_max, y_max; + uint64_t screen_area = (uint64_t)screen_width * (uint64_t)screen_height; + + console_query_mode(&x_max, &y_max); + text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (uint64_t)x_max * (uint64_t)y_max; + + if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO) + keep = true; + } + + if (keep) + return ST->ConOut->Mode->Mode; + } + + /* If we reached here, then we have a high resolution screen and the text + * viewport is less than 10% the screen area, so the firmware developer + * screwed up. Try to switch to a better mode. Mode number 2 is first non + * standard mode, which is provided by the device manufacturer, so it should + * be a good mode. + * Note: MaxMode is the number of modes, not the last mode. */ + if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_FIRMWARE_FIRST) + return CONSOLE_MODE_FIRMWARE_FIRST; + + /* Try again with mode different than zero (assume user requests + * auto mode due to some problem with mode zero). */ + if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_80_50) + return CONSOLE_MODE_80_50; + + return CONSOLE_MODE_80_25; +} + +EFI_STATUS console_set_mode(int64_t mode) { + switch (mode) { + case CONSOLE_MODE_KEEP: + /* If the firmware indicates the current mode is invalid, change it anyway. */ + if (ST->ConOut->Mode->Mode < CONSOLE_MODE_RANGE_MIN) + return change_mode(CONSOLE_MODE_RANGE_MIN); + return EFI_SUCCESS; + + case CONSOLE_MODE_NEXT: + if (ST->ConOut->Mode->MaxMode <= CONSOLE_MODE_RANGE_MIN) + return EFI_UNSUPPORTED; + + mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode); + do { + mode = (mode + 1) % ST->ConOut->Mode->MaxMode; + if (change_mode(mode) == EFI_SUCCESS) + break; + /* If this mode is broken/unsupported, try the next. + * If mode is 0, we wrapped around and should stop. */ + } while (mode > CONSOLE_MODE_RANGE_MIN); + + return EFI_SUCCESS; + + case CONSOLE_MODE_AUTO: + return change_mode(get_auto_mode()); + + case CONSOLE_MODE_FIRMWARE_MAX: + /* Note: MaxMode is the number of modes, not the last mode. */ + return change_mode(ST->ConOut->Mode->MaxMode - 1LL); + + default: + return change_mode(mode); + } +} + +EFI_STATUS console_query_mode(size_t *x_max, size_t *y_max) { + EFI_STATUS err; + + assert(x_max); + assert(y_max); + + err = ST->ConOut->QueryMode(ST->ConOut, ST->ConOut->Mode->Mode, x_max, y_max); + if (err != EFI_SUCCESS) { + /* Fallback values mandated by UEFI spec. */ + switch (ST->ConOut->Mode->Mode) { + case CONSOLE_MODE_80_50: + *x_max = 80; + *y_max = 50; + break; + case CONSOLE_MODE_80_25: + default: + *x_max = 80; + *y_max = 25; + } + } + + return err; +} diff --git a/src/boot/efi/console.h b/src/boot/efi/console.h new file mode 100644 index 0000000..c4d821a --- /dev/null +++ b/src/boot/efi/console.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/simple-text-io.h" + +enum { + EFI_SHIFT_PRESSED = EFI_RIGHT_SHIFT_PRESSED|EFI_LEFT_SHIFT_PRESSED, + EFI_CONTROL_PRESSED = EFI_RIGHT_CONTROL_PRESSED|EFI_LEFT_CONTROL_PRESSED, + EFI_ALT_PRESSED = EFI_RIGHT_ALT_PRESSED|EFI_LEFT_ALT_PRESSED, + EFI_LOGO_PRESSED = EFI_RIGHT_LOGO_PRESSED|EFI_LEFT_LOGO_PRESSED, +}; + +#define KEYPRESS(keys, scan, uni) ((((uint64_t)keys) << 32) | (((uint64_t)scan) << 16) | (uni)) +#define KEYCHAR(k) ((char16_t)(k)) +#define CHAR_CTRL(c) ((c) - 'a' + 1) + +enum { + /* Console mode is a int32_t in EFI. We use int64_t to make room for our special values. */ + CONSOLE_MODE_RANGE_MIN = 0, + CONSOLE_MODE_RANGE_MAX = INT32_MAX, /* This is just the theoretical limit. */ + CONSOLE_MODE_INVALID = -1, /* UEFI uses -1 if the device is not in a valid text mode. */ + + CONSOLE_MODE_80_25 = 0, /* 80x25 is required by UEFI spec. */ + CONSOLE_MODE_80_50 = 1, /* 80x50 may be supported. */ + CONSOLE_MODE_FIRMWARE_FIRST = 2, /* First custom mode, if supported. */ + + /* These are our own mode values that map to concrete values at runtime. */ + CONSOLE_MODE_KEEP = CONSOLE_MODE_RANGE_MAX + 1LL, + CONSOLE_MODE_NEXT, + CONSOLE_MODE_AUTO, + CONSOLE_MODE_FIRMWARE_MAX, /* 'max' in config. */ +}; + +EFI_STATUS console_key_read(uint64_t *key, uint64_t timeout_usec); +EFI_STATUS console_set_mode(int64_t mode); +EFI_STATUS console_query_mode(size_t *x_max, size_t *y_max); +EFI_STATUS query_screen_resolution(uint32_t *ret_width, uint32_t *ret_height); diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c new file mode 100644 index 0000000..5b90e17 --- /dev/null +++ b/src/boot/efi/cpio.c @@ -0,0 +1,512 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "device-path-util.h" +#include "measure.h" +#include "proto/device-path.h" +#include "util.h" + +static char *write_cpio_word(char *p, uint32_t v) { + static const char hex[] = "0123456789abcdef"; + + assert(p); + + /* Writes a CPIO header 8 character hex value */ + + for (size_t i = 0; i < 8; i++) + p[7-i] = hex[(v >> (4 * i)) & 0xF]; + + return p + 8; +} + +static char *mangle_filename(char *p, const char16_t *f) { + char* w; + + assert(p); + assert(f); + + /* Basically converts UTF-16 to plain ASCII (note that we filtered non-ASCII filenames beforehand, so + * this operation is always safe) */ + + for (w = p; *f != 0; f++) { + assert(*f <= 0x7fu); + + *(w++) = *f; + } + + *(w++) = 0; + return w; +} + +static char *pad4(char *p, const char *start) { + assert(p); + assert(start); + assert(p >= start); + + /* Appends NUL bytes to 'p', until the address is divisible by 4, when taken relative to 'start' */ + + while ((p - start) % 4 != 0) + *(p++) = 0; + + return p; +} + +static EFI_STATUS pack_cpio_one( + const char16_t *fname, + const void *contents, + size_t contents_size, + const char *target_dir_prefix, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + size_t l, target_dir_prefix_size, fname_size, q; + char *a; + + assert(fname); + assert(contents_size || contents_size == 0); + assert(target_dir_prefix); + assert(inode_counter); + assert(cpio_buffer); + assert(cpio_buffer_size); + + /* Serializes one file in the cpio format understood by the kernel initrd logic. + * + * See: https://docs.kernel.org/driver-api/early-userspace/buffer-format.html */ + + if (contents_size > UINT32_MAX) /* cpio cannot deal with > 32-bit file sizes */ + return EFI_LOAD_ERROR; + + if (*inode_counter == UINT32_MAX) /* more than 2^32-1 inodes? yikes. cpio doesn't support that either */ + return EFI_OUT_OF_RESOURCES; + + l = 6 + 13*8 + 1 + 1; /* Fixed CPIO header size, slash separator, and NUL byte after the file name */ + + target_dir_prefix_size = strlen8(target_dir_prefix); + if (l > SIZE_MAX - target_dir_prefix_size) + return EFI_OUT_OF_RESOURCES; + l += target_dir_prefix_size; + + fname_size = strlen16(fname); + if (l > SIZE_MAX - fname_size) + return EFI_OUT_OF_RESOURCES; + l += fname_size; /* append space for file name */ + + /* CPIO can't deal with fnames longer than 2^32-1 */ + if (target_dir_prefix_size + fname_size >= UINT32_MAX) + return EFI_OUT_OF_RESOURCES; + + /* Align the whole header to 4 byte size */ + l = ALIGN4(l); + if (l == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + /* Align the contents to 4 byte size */ + q = ALIGN4(contents_size); + if (q == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (l > SIZE_MAX - q) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + l += q; /* Add contents to header */ + + if (*cpio_buffer_size > SIZE_MAX - l) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l); + + *cpio_buffer = a; + a = (char *) *cpio_buffer + *cpio_buffer_size; + + a = mempcpy(a, "070701", 6); /* magic ID */ + + a = write_cpio_word(a, (*inode_counter)++); /* inode */ + a = write_cpio_word(a, access_mode | 0100000 /* = S_IFREG */); /* mode */ + a = write_cpio_word(a, 0); /* uid */ + a = write_cpio_word(a, 0); /* gid */ + a = write_cpio_word(a, 1); /* nlink */ + + /* Note: we don't make any attempt to propagate the mtime here, for two reasons: it's a mess given + * that FAT usually is assumed to operate with timezoned timestamps, while UNIX does not. More + * importantly though: the modifications times would hamper our goals of providing stable + * measurements for the same boots. After all we extend the initrds we generate here into TPM2 + * PCRs. */ + a = write_cpio_word(a, 0); /* mtime */ + a = write_cpio_word(a, contents_size); /* size */ + a = write_cpio_word(a, 0); /* major(dev) */ + a = write_cpio_word(a, 0); /* minor(dev) */ + a = write_cpio_word(a, 0); /* major(rdev) */ + a = write_cpio_word(a, 0); /* minor(rdev) */ + a = write_cpio_word(a, target_dir_prefix_size + fname_size + 2); /* fname size */ + a = write_cpio_word(a, 0); /* "crc" */ + + a = mempcpy(a, target_dir_prefix, target_dir_prefix_size); + *(a++) = '/'; + a = mangle_filename(a, fname); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + a = mempcpy(a, contents, contents_size); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l); + *cpio_buffer_size += l; + + return EFI_SUCCESS; +} + +static EFI_STATUS pack_cpio_dir( + const char *path, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + size_t l, path_size; + char *a; + + assert(path); + assert(inode_counter); + assert(cpio_buffer); + assert(cpio_buffer_size); + + /* Serializes one directory inode in cpio format. Note that cpio archives must first create the dirs + * they want to place files in. */ + + if (*inode_counter == UINT32_MAX) + return EFI_OUT_OF_RESOURCES; + + l = 6 + 13*8 + 1; /* Fixed CPIO header size, and NUL byte after the file name */ + + path_size = strlen8(path); + if (l > SIZE_MAX - path_size) + return EFI_OUT_OF_RESOURCES; + l += path_size; + + /* Align the whole header to 4 byte size */ + l = ALIGN4(l); + if (l == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (*cpio_buffer_size > SIZE_MAX - l) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + *cpio_buffer = a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l); + a = (char *) *cpio_buffer + *cpio_buffer_size; + + a = mempcpy(a, "070701", 6); /* magic ID */ + + a = write_cpio_word(a, (*inode_counter)++); /* inode */ + a = write_cpio_word(a, access_mode | 0040000 /* = S_IFDIR */); /* mode */ + a = write_cpio_word(a, 0); /* uid */ + a = write_cpio_word(a, 0); /* gid */ + a = write_cpio_word(a, 1); /* nlink */ + a = write_cpio_word(a, 0); /* mtime */ + a = write_cpio_word(a, 0); /* size */ + a = write_cpio_word(a, 0); /* major(dev) */ + a = write_cpio_word(a, 0); /* minor(dev) */ + a = write_cpio_word(a, 0); /* major(rdev) */ + a = write_cpio_word(a, 0); /* minor(rdev) */ + a = write_cpio_word(a, path_size + 1); /* fname size */ + a = write_cpio_word(a, 0); /* "crc" */ + + a = mempcpy(a, path, path_size + 1); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l); + + *cpio_buffer_size += l; + return EFI_SUCCESS; +} + +static EFI_STATUS pack_cpio_prefix( + const char *path, + uint32_t dir_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + EFI_STATUS err; + + assert(path); + assert(inode_counter); + assert(cpio_buffer); + assert(cpio_buffer_size); + + /* Serializes directory inodes of all prefix paths of the specified path in cpio format. Note that + * (similar to mkdir -p behaviour) all leading paths are created with 0555 access mode, only the + * final dir is created with the specified directory access mode. */ + + for (const char *p = path;;) { + const char *e; + + e = strchr8(p, '/'); + if (!e) + break; + + if (e > p) { + _cleanup_free_ char *t = NULL; + + t = xstrndup8(path, e - path); + if (!t) + return EFI_OUT_OF_RESOURCES; + + err = pack_cpio_dir(t, 0555, inode_counter, cpio_buffer, cpio_buffer_size); + if (err != EFI_SUCCESS) + return err; + } + + p = e + 1; + } + + return pack_cpio_dir(path, dir_mode, inode_counter, cpio_buffer, cpio_buffer_size); +} + +static EFI_STATUS pack_cpio_trailer( + void **cpio_buffer, + size_t *cpio_buffer_size) { + + static const char trailer[] = + "070701" + "00000000" + "00000000" + "00000000" + "00000000" + "00000001" + "00000000" + "00000000" + "00000000" + "00000000" + "00000000" + "00000000" + "0000000B" + "00000000" + "TRAILER!!!\0\0\0"; /* There's a fourth NUL byte appended here, because this is a string */ + + /* Generates the cpio trailer record that indicates the end of our initrd cpio archive */ + + assert(cpio_buffer); + assert(cpio_buffer_size); + assert_cc(sizeof(trailer) % 4 == 0); + + *cpio_buffer = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + sizeof(trailer)); + memcpy((uint8_t*) *cpio_buffer + *cpio_buffer_size, trailer, sizeof(trailer)); + *cpio_buffer_size += sizeof(trailer); + + return EFI_SUCCESS; +} + +EFI_STATUS pack_cpio( + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *dropin_dir, + const char16_t *match_suffix, + const char *target_dir_prefix, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured) { + + _cleanup_(file_closep) EFI_FILE *root = NULL, *extra_dir = NULL; + size_t dirent_size = 0, buffer_size = 0, n_items = 0, n_allocated = 0; + _cleanup_free_ char16_t *rel_dropin_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + _cleanup_(strv_freep) char16_t **items = NULL; + _cleanup_free_ void *buffer = NULL; + uint32_t inode = 1; /* inode counter, so that each item gets a new inode */ + EFI_STATUS err; + + assert(loaded_image); + assert(target_dir_prefix); + assert(ret_buffer); + assert(ret_buffer_size); + + if (!loaded_image->DeviceHandle) + goto nothing; + + err = open_volume(loaded_image->DeviceHandle, &root); + if (err == EFI_UNSUPPORTED) + /* Error will be unsupported if the bootloader doesn't implement the file system protocol on + * its file handles. */ + goto nothing; + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + if (!dropin_dir) { + dropin_dir = rel_dropin_dir = get_extra_dir(loaded_image->FilePath); + if (!dropin_dir) + goto nothing; + } + + err = open_directory(root, dropin_dir, &extra_dir); + if (err == EFI_NOT_FOUND) + /* No extra subdir, that's totally OK */ + goto nothing; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open extra directory of loaded image: %m"); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read extra directory of loaded image: %m"); + if (!dirent) /* End of directory */ + break; + + if (dirent->FileName[0] == '.') + continue; + if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (match_suffix && !endswith_no_case(dirent->FileName, match_suffix)) + continue; + if (!is_ascii(dirent->FileName)) + continue; + if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */ + continue; + + d = xstrdup16(dirent->FileName); + + if (n_items+2 > n_allocated) { + /* We allocate 16 entries at a time, as a matter of optimization */ + if (n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + size_t m = n_items + 16; + items = xrealloc(items, n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *)); + n_allocated = m; + } + + items[n_items++] = TAKE_PTR(d); + items[n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */ + } + + if (n_items == 0) + /* Empty directory */ + goto nothing; + + /* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements + * are not dependent on read order) */ + sort_pointer_array((void**) items, n_items, (compare_pointer_func_t) strcmp16); + + /* Generate the leading directory inodes right before adding the first files, to the + * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't exist. */ + err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio prefix: %m"); + + for (size_t i = 0; i < n_items; i++) { + _cleanup_free_ char *content = NULL; + size_t contentsize = 0; /* avoid false maybe-uninitialized warning */ + + err = file_read(extra_dir, items[i], 0, 0, &content, &contentsize); + if (err != EFI_SUCCESS) { + log_error_status(err, "Failed to read %ls, ignoring: %m", items[i]); + continue; + } + + err = pack_cpio_one( + items[i], + content, contentsize, + target_dir_prefix, + access_mode, + &inode, + &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio file %ls: %m", dirent->FileName); + } + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *ret_buffer = TAKE_PTR(buffer); + *ret_buffer_size = buffer_size; + + return EFI_SUCCESS; + +nothing: + *ret_buffer = NULL; + *ret_buffer_size = 0; + + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; +} + +EFI_STATUS pack_cpio_literal( + const void *data, + size_t data_size, + const char *target_dir_prefix, + const char16_t *target_filename, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured) { + + uint32_t inode = 1; /* inode counter, so that each item gets a new inode */ + _cleanup_free_ void *buffer = NULL; + size_t buffer_size = 0; + EFI_STATUS err; + + assert(data || data_size == 0); + assert(target_dir_prefix); + assert(target_filename); + assert(ret_buffer); + assert(ret_buffer_size); + + /* Generate the leading directory inodes right before adding the first files, to the + * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't exist. */ + + err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio prefix: %m"); + + err = pack_cpio_one( + target_filename, + data, data_size, + target_dir_prefix, + access_mode, + &inode, + &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio file %ls: %m", target_filename); + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *ret_buffer = TAKE_PTR(buffer); + *ret_buffer_size = buffer_size; + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/cpio.h b/src/boot/efi/cpio.h new file mode 100644 index 0000000..26851e3 --- /dev/null +++ b/src/boot/efi/cpio.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/loaded-image.h" + +EFI_STATUS pack_cpio( + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *dropin_dir, + const char16_t *match_suffix, + const char *target_dir_prefix, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured); + +EFI_STATUS pack_cpio_literal( + const void *data, + size_t data_size, + const char *target_dir_prefix, + const char16_t *target_filename, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured); diff --git a/src/boot/efi/device-path-util.c b/src/boot/efi/device-path-util.c new file mode 100644 index 0000000..2a85e8b --- /dev/null +++ b/src/boot/efi/device-path-util.c @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "util.h" + +EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp) { + EFI_STATUS err; + EFI_DEVICE_PATH *dp; + + assert(file); + assert(ret_dp); + + err = BS->HandleProtocol(device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *end_node = dp; + while (!device_path_is_end(end_node)) + end_node = device_path_next_node(end_node); + + size_t file_size = strsize16(file); + size_t dp_size = (uint8_t *) end_node - (uint8_t *) dp; + + /* Make a copy that can also hold a file media device path. */ + *ret_dp = xmalloc(dp_size + file_size + sizeof(FILEPATH_DEVICE_PATH) + sizeof(EFI_DEVICE_PATH)); + dp = mempcpy(*ret_dp, dp, dp_size); + + FILEPATH_DEVICE_PATH *file_dp = (FILEPATH_DEVICE_PATH *) dp; + file_dp->Header = (EFI_DEVICE_PATH) { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_FILEPATH_DP, + .Length = sizeof(FILEPATH_DEVICE_PATH) + file_size, + }; + memcpy(file_dp->PathName, file, file_size); + + dp = device_path_next_node(dp); + *dp = DEVICE_PATH_END_NODE; + return EFI_SUCCESS; +} + +static char16_t *device_path_to_str_internal(const EFI_DEVICE_PATH *dp) { + char16_t *str = NULL; + + for (const EFI_DEVICE_PATH *node = dp; !device_path_is_end(node); node = device_path_next_node(node)) { + _cleanup_free_ char16_t *old = str; + + if (node->Type == END_DEVICE_PATH_TYPE && node->SubType == END_INSTANCE_DEVICE_PATH_SUBTYPE) { + str = xasprintf("%ls%s,", strempty(old), old ? "\\" : ""); + continue; + } + + /* Special-case this so that FilePath-only device path string look and behave nicely. */ + if (node->Type == MEDIA_DEVICE_PATH && node->SubType == MEDIA_FILEPATH_DP) { + str = xasprintf("%ls%s%ls", + strempty(old), + old ? "\\" : "", + ((FILEPATH_DEVICE_PATH *) node)->PathName); + continue; + } + + /* Instead of coding all the different types and sub-types here we just use the + * generic node form. This function is a best-effort for firmware that does not + * provide the EFI_DEVICE_PATH_TO_TEXT_PROTOCOL after all. */ + + size_t size = node->Length - sizeof(EFI_DEVICE_PATH); + _cleanup_free_ char16_t *hex_data = hexdump((uint8_t *) node + sizeof(EFI_DEVICE_PATH), size); + str = xasprintf("%ls%sPath(%u,%u%s%ls)", + strempty(old), + old ? "/" : "", + node->Type, + node->SubType, + size == 0 ? "" : ",", + hex_data); + } + + return str; +} + +EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret) { + EFI_DEVICE_PATH_TO_TEXT_PROTOCOL *dp_to_text; + EFI_STATUS err; + _cleanup_free_ char16_t *str = NULL; + + assert(dp); + assert(ret); + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_DEVICE_PATH_TO_TEXT_PROTOCOL), NULL, (void **) &dp_to_text); + if (err != EFI_SUCCESS) { + *ret = device_path_to_str_internal(dp); + return EFI_SUCCESS; + } + + str = dp_to_text->ConvertDevicePathToText(dp, false, false); + if (!str) + return EFI_OUT_OF_RESOURCES; + + *ret = TAKE_PTR(str); + return EFI_SUCCESS; +} + +bool device_path_startswith(const EFI_DEVICE_PATH *dp, const EFI_DEVICE_PATH *start) { + if (!start) + return true; + if (!dp) + return false; + for (;;) { + if (device_path_is_end(start)) + return true; + if (device_path_is_end(dp)) + return false; + if (start->Length != dp->Length) + return false; + if (memcmp(dp, start, start->Length) != 0) + return false; + start = device_path_next_node(start); + dp = device_path_next_node(dp); + } +} + +EFI_DEVICE_PATH *device_path_replace_node( + const EFI_DEVICE_PATH *path, const EFI_DEVICE_PATH *node, const EFI_DEVICE_PATH *new_node) { + + /* Create a new device path as a copy of path, while chopping off the remainder starting at the given + * node. If new_node is provided, it is appended at the end of the new path. */ + + assert(path); + assert(node); + + size_t len = (uint8_t *) node - (uint8_t *) path; + EFI_DEVICE_PATH *ret = xmalloc(len + (new_node ? new_node->Length : 0) + sizeof(EFI_DEVICE_PATH)); + EFI_DEVICE_PATH *end = mempcpy(ret, path, len); + + if (new_node) + end = mempcpy(end, new_node, new_node->Length); + + *end = DEVICE_PATH_END_NODE; + return ret; +} diff --git a/src/boot/efi/device-path-util.h b/src/boot/efi/device-path-util.h new file mode 100644 index 0000000..08f1a9c --- /dev/null +++ b/src/boot/efi/device-path-util.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "proto/device-path.h" + +EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp); +EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret); +bool device_path_startswith(const EFI_DEVICE_PATH *dp, const EFI_DEVICE_PATH *start); +EFI_DEVICE_PATH *device_path_replace_node( + const EFI_DEVICE_PATH *path, const EFI_DEVICE_PATH *node, const EFI_DEVICE_PATH *new_node); + +static inline EFI_DEVICE_PATH *device_path_next_node(const EFI_DEVICE_PATH *dp) { + assert(dp); + return (EFI_DEVICE_PATH *) ((uint8_t *) dp + dp->Length); +} + +static inline bool device_path_is_end(const EFI_DEVICE_PATH *dp) { + assert(dp); + return dp->Type == END_DEVICE_PATH_TYPE && dp->SubType == END_ENTIRE_DEVICE_PATH_SUBTYPE; +} + +#define DEVICE_PATH_END_NODE \ + (EFI_DEVICE_PATH) { \ + .Type = END_DEVICE_PATH_TYPE, \ + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, \ + .Length = sizeof(EFI_DEVICE_PATH) \ + } diff --git a/src/boot/efi/devicetree.c b/src/boot/efi/devicetree.c new file mode 100644 index 0000000..61a43cd --- /dev/null +++ b/src/boot/efi/devicetree.c @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "devicetree.h" +#include "proto/dt-fixup.h" +#include "util.h" + +#define FDT_V1_SIZE (7*4) + +static EFI_STATUS devicetree_allocate(struct devicetree_state *state, size_t size) { + size_t pages = DIV_ROUND_UP(size, EFI_PAGE_SIZE); + EFI_STATUS err; + + assert(state); + + err = BS->AllocatePages(AllocateAnyPages, EfiACPIReclaimMemory, pages, &state->addr); + if (err != EFI_SUCCESS) + return err; + + state->pages = pages; + return err; +} + +static size_t devicetree_allocated(const struct devicetree_state *state) { + assert(state); + return state->pages * EFI_PAGE_SIZE; +} + +static EFI_STATUS devicetree_fixup(struct devicetree_state *state, size_t len) { + EFI_DT_FIXUP_PROTOCOL *fixup; + size_t size; + EFI_STATUS err; + + assert(state); + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_DT_FIXUP_PROTOCOL), NULL, (void **) &fixup); + /* Skip fixup if we cannot locate device tree fixup protocol */ + if (err != EFI_SUCCESS) + return EFI_SUCCESS; + + size = devicetree_allocated(state); + err = fixup->Fixup(fixup, PHYSICAL_ADDRESS_TO_POINTER(state->addr), &size, + EFI_DT_APPLY_FIXUPS | EFI_DT_RESERVE_MEMORY); + if (err == EFI_BUFFER_TOO_SMALL) { + EFI_PHYSICAL_ADDRESS oldaddr = state->addr; + size_t oldpages = state->pages; + void *oldptr = PHYSICAL_ADDRESS_TO_POINTER(state->addr); + + err = devicetree_allocate(state, size); + if (err != EFI_SUCCESS) + return err; + + memcpy(PHYSICAL_ADDRESS_TO_POINTER(state->addr), oldptr, len); + err = BS->FreePages(oldaddr, oldpages); + if (err != EFI_SUCCESS) + return err; + + size = devicetree_allocated(state); + err = fixup->Fixup(fixup, PHYSICAL_ADDRESS_TO_POINTER(state->addr), &size, + EFI_DT_APPLY_FIXUPS | EFI_DT_RESERVE_MEMORY); + } + + return err; +} + +EFI_STATUS devicetree_install(struct devicetree_state *state, EFI_FILE *root_dir, char16_t *name) { + _cleanup_(file_closep) EFI_FILE *handle = NULL; + _cleanup_free_ EFI_FILE_INFO *info = NULL; + size_t len; + EFI_STATUS err; + + assert(state); + assert(root_dir); + assert(name); + + /* Capture the original value for the devicetree table. NULL is not an error in this case so we don't + * need to check the return value. NULL simply means the system fw had no devicetree initially (and + * is the correct value to use to return to the initial state if needed). */ + state->orig = find_configuration_table(MAKE_GUID_PTR(EFI_DTB_TABLE)); + + err = root_dir->Open(root_dir, &handle, name, EFI_FILE_MODE_READ, EFI_FILE_READ_ONLY); + if (err != EFI_SUCCESS) + return err; + + err = get_file_info(handle, &info, NULL); + if (err != EFI_SUCCESS) + return err; + if (info->FileSize < FDT_V1_SIZE || info->FileSize > 32 * 1024 * 1024) + /* 32MB device tree blob doesn't seem right */ + return EFI_INVALID_PARAMETER; + + len = info->FileSize; + + err = devicetree_allocate(state, len); + if (err != EFI_SUCCESS) + return err; + + err = handle->Read(handle, &len, PHYSICAL_ADDRESS_TO_POINTER(state->addr)); + if (err != EFI_SUCCESS) + return err; + + err = devicetree_fixup(state, len); + if (err != EFI_SUCCESS) + return err; + + return BS->InstallConfigurationTable( + MAKE_GUID_PTR(EFI_DTB_TABLE), PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +EFI_STATUS devicetree_install_from_memory( + struct devicetree_state *state, const void *dtb_buffer, size_t dtb_length) { + + EFI_STATUS err; + + assert(state); + assert(dtb_buffer && dtb_length > 0); + + /* Capture the original value for the devicetree table. NULL is not an error in this case so we don't + * need to check the return value. NULL simply means the system fw had no devicetree initially (and + * is the correct value to use to return to the initial state if needed). */ + state->orig = find_configuration_table(MAKE_GUID_PTR(EFI_DTB_TABLE)); + + err = devicetree_allocate(state, dtb_length); + if (err != EFI_SUCCESS) + return err; + + memcpy(PHYSICAL_ADDRESS_TO_POINTER(state->addr), dtb_buffer, dtb_length); + + err = devicetree_fixup(state, dtb_length); + if (err != EFI_SUCCESS) + return err; + + return BS->InstallConfigurationTable( + MAKE_GUID_PTR(EFI_DTB_TABLE), PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +void devicetree_cleanup(struct devicetree_state *state) { + EFI_STATUS err; + + if (!state->pages) + return; + + err = BS->InstallConfigurationTable(MAKE_GUID_PTR(EFI_DTB_TABLE), state->orig); + /* don't free the current device tree if we can't reinstate the old one */ + if (err != EFI_SUCCESS) + return; + + BS->FreePages(state->addr, state->pages); + state->pages = 0; +} diff --git a/src/boot/efi/devicetree.h b/src/boot/efi/devicetree.h new file mode 100644 index 0000000..33eaa22 --- /dev/null +++ b/src/boot/efi/devicetree.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +struct devicetree_state { + EFI_PHYSICAL_ADDRESS addr; + size_t pages; + void *orig; +}; + +EFI_STATUS devicetree_install(struct devicetree_state *state, EFI_FILE *root_dir, char16_t *name); +EFI_STATUS devicetree_install_from_memory( + struct devicetree_state *state, const void *dtb_buffer, size_t dtb_length); +void devicetree_cleanup(struct devicetree_state *state); diff --git a/src/boot/efi/drivers.c b/src/boot/efi/drivers.c new file mode 100644 index 0000000..0674557 --- /dev/null +++ b/src/boot/efi/drivers.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "drivers.h" +#include "util.h" + +static EFI_STATUS load_one_driver( + EFI_HANDLE parent_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *fname) { + + _cleanup_(unload_imagep) EFI_HANDLE image = NULL; + _cleanup_free_ EFI_DEVICE_PATH *path = NULL; + _cleanup_free_ char16_t *spath = NULL; + EFI_STATUS err; + + assert(parent_image); + assert(loaded_image); + assert(fname); + + spath = xasprintf("\\EFI\\systemd\\drivers\\%ls", fname); + err = make_file_device_path(loaded_image->DeviceHandle, spath, &path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making file device path: %m"); + + err = BS->LoadImage(false, parent_image, path, NULL, 0, &image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to load image %ls: %m", fname); + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to find protocol in driver image %ls: %m", fname); + + if (loaded_image->ImageCodeType != EfiBootServicesCode && + loaded_image->ImageCodeType != EfiRuntimeServicesCode) + return log_error("Image %ls is not a driver, refusing.", fname); + + err = BS->StartImage(image, NULL, NULL); + if (err != EFI_SUCCESS) { + /* EFI_ABORTED signals an initializing driver. It uses this error code on success + * so that it is unloaded after. */ + if (err != EFI_ABORTED) + log_error_status(err, "Failed to start image %ls: %m", fname); + return err; + } + + TAKE_PTR(image); + return EFI_SUCCESS; +} + +EFI_STATUS reconnect_all_drivers(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles = 0; + EFI_STATUS err; + + /* Reconnects all handles, so that any loaded drivers can take effect. */ + + err = BS->LocateHandleBuffer(AllHandles, NULL, NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to get list of handles: %m"); + + for (size_t i = 0; i < n_handles; i++) + /* Some firmware gives us some bogus handles (or they might become bad due to + * reconnecting everything). Security policy may also prevent us from doing so too. + * There is nothing we can realistically do on errors anyways, so just ignore them. */ + (void) BS->ConnectController(handles[i], NULL, NULL, true); + + return EFI_SUCCESS; +} + +EFI_STATUS load_drivers( + EFI_HANDLE parent_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + EFI_FILE *root_dir) { + + _cleanup_(file_closep) EFI_FILE *drivers_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + size_t dirent_size = 0, n_succeeded = 0; + EFI_STATUS err; + + err = open_directory( + root_dir, + u"\\EFI\\systemd\\drivers", + &drivers_dir); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open \\EFI\\systemd\\drivers: %m"); + + for (;;) { + err = readdir(drivers_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read extra directory of loaded image: %m"); + if (!dirent) /* End of directory */ + break; + + if (dirent->FileName[0] == '.') + continue; + if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (!endswith_no_case(dirent->FileName, EFI_MACHINE_TYPE_NAME u".efi")) + continue; + + err = load_one_driver(parent_image, loaded_image, dirent->FileName); + if (err != EFI_SUCCESS) + continue; + + n_succeeded++; + } + + if (n_succeeded > 0) + (void) reconnect_all_drivers(); + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/drivers.h b/src/boot/efi/drivers.h new file mode 100644 index 0000000..ecd0b4e --- /dev/null +++ b/src/boot/efi/drivers.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "proto/loaded-image.h" + +EFI_STATUS reconnect_all_drivers(void); +EFI_STATUS load_drivers( + EFI_HANDLE parent_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + EFI_FILE *root_dir); diff --git a/src/boot/efi/efi-string.c b/src/boot/efi/efi-string.c new file mode 100644 index 0000000..4144c0d --- /dev/null +++ b/src/boot/efi/efi-string.c @@ -0,0 +1,1084 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "efi-string.h" + +#if SD_BOOT +# include "proto/simple-text-io.h" +# include "util.h" +#else +# include +# include "alloc-util.h" +# define xnew(t, n) ASSERT_SE_PTR(new(t, n)) +# define xmalloc(n) ASSERT_SE_PTR(malloc(n)) +#endif + +/* String functions for both char and char16_t that should behave the same way as their respective + * counterpart in userspace. Where it makes sense, these accept NULL and do something sensible whereas + * userspace does not allow for this (strlen8(NULL) returns 0 like strlen_ptr(NULL) for example). To make it + * easier to tell in code which kind of string they work on, we use 8/16 suffixes. This also makes is easier + * to unit test them. */ + +#define DEFINE_STRNLEN(type, name) \ + size_t name(const type *s, size_t n) { \ + if (!s) \ + return 0; \ + \ + size_t len = 0; \ + while (len < n && *s) { \ + s++; \ + len++; \ + } \ + \ + return len; \ + } + +DEFINE_STRNLEN(char, strnlen8); +DEFINE_STRNLEN(char16_t, strnlen16); + +#define TOLOWER(c) \ + ({ \ + typeof(c) _c = (c); \ + (_c >= 'A' && _c <= 'Z') ? _c + ('a' - 'A') : _c; \ + }) + +#define DEFINE_STRTOLOWER(type, name) \ + void name(type *s) { \ + if (!s) \ + return; \ + for (; *s; s++) \ + *s = TOLOWER(*s); \ + } + +DEFINE_STRTOLOWER(char, strtolower8); +DEFINE_STRTOLOWER(char16_t, strtolower16); + +#define DEFINE_STRNCASECMP(type, name, tolower) \ + int name(const type *s1, const type *s2, size_t n) { \ + if (!s1 || !s2) \ + return CMP(s1, s2); \ + \ + while (n > 0) { \ + type c1 = *s1, c2 = *s2; \ + if (tolower) { \ + c1 = TOLOWER(c1); \ + c2 = TOLOWER(c2); \ + } \ + if (!c1 || c1 != c2) \ + return CMP(c1, c2); \ + \ + s1++; \ + s2++; \ + n--; \ + } \ + \ + return 0; \ + } + +DEFINE_STRNCASECMP(char, strncmp8, false); +DEFINE_STRNCASECMP(char16_t, strncmp16, false); +DEFINE_STRNCASECMP(char, strncasecmp8, true); +DEFINE_STRNCASECMP(char16_t, strncasecmp16, true); + +#define DEFINE_STRCPY(type, name) \ + type *name(type * restrict dest, const type * restrict src) { \ + type *ret = ASSERT_PTR(dest); \ + \ + if (!src) { \ + *dest = '\0'; \ + return ret; \ + } \ + \ + while (*src) { \ + *dest = *src; \ + dest++; \ + src++; \ + } \ + \ + *dest = '\0'; \ + return ret; \ + } + +DEFINE_STRCPY(char, strcpy8); +DEFINE_STRCPY(char16_t, strcpy16); + +#define DEFINE_STRCHR(type, name) \ + type *name(const type *s, type c) { \ + if (!s) \ + return NULL; \ + \ + while (*s) { \ + if (*s == c) \ + return (type *) s; \ + s++; \ + } \ + \ + return c ? NULL : (type *) s; \ + } + +DEFINE_STRCHR(char, strchr8); +DEFINE_STRCHR(char16_t, strchr16); + +#define DEFINE_STRNDUP(type, name, len_func) \ + type *name(const type *s, size_t n) { \ + if (!s) \ + return NULL; \ + \ + size_t len = len_func(s, n); \ + size_t size = len * sizeof(type); \ + \ + type *dup = xmalloc(size + sizeof(type)); \ + if (size > 0) \ + memcpy(dup, s, size); \ + dup[len] = '\0'; \ + \ + return dup; \ + } + +DEFINE_STRNDUP(char, xstrndup8, strnlen8); +DEFINE_STRNDUP(char16_t, xstrndup16, strnlen16); + +static unsigned utf8_to_unichar(const char *utf8, size_t n, char32_t *c) { + char32_t unichar; + unsigned len; + + assert(utf8); + assert(c); + + if (!(utf8[0] & 0x80)) { + *c = utf8[0]; + return 1; + } else if ((utf8[0] & 0xe0) == 0xc0) { + len = 2; + unichar = utf8[0] & 0x1f; + } else if ((utf8[0] & 0xf0) == 0xe0) { + len = 3; + unichar = utf8[0] & 0x0f; + } else if ((utf8[0] & 0xf8) == 0xf0) { + len = 4; + unichar = utf8[0] & 0x07; + } else if ((utf8[0] & 0xfc) == 0xf8) { + len = 5; + unichar = utf8[0] & 0x03; + } else if ((utf8[0] & 0xfe) == 0xfc) { + len = 6; + unichar = utf8[0] & 0x01; + } else { + *c = UINT32_MAX; + return 1; + } + + if (len > n) { + *c = UINT32_MAX; + return len; + } + + for (unsigned i = 1; i < len; i++) { + if ((utf8[i] & 0xc0) != 0x80) { + *c = UINT32_MAX; + return len; + } + unichar <<= 6; + unichar |= utf8[i] & 0x3f; + } + + *c = unichar; + return len; +} + +/* Convert UTF-8 to UCS-2, skipping any invalid or short byte sequences. */ +char16_t *xstrn8_to_16(const char *str8, size_t n) { + if (!str8 || n == 0) + return NULL; + + size_t i = 0; + char16_t *str16 = xnew(char16_t, n + 1); + + while (n > 0 && *str8 != '\0') { + char32_t unichar; + + size_t utf8len = utf8_to_unichar(str8, n, &unichar); + str8 += utf8len; + n = LESS_BY(n, utf8len); + + switch (unichar) { + case 0 ... 0xd7ffU: + case 0xe000U ... 0xffffU: + str16[i++] = unichar; + break; + } + } + + str16[i] = '\0'; + return str16; +} + +char *startswith8(const char *s, const char *prefix) { + size_t l; + + assert(prefix); + + if (!s) + return NULL; + + l = strlen8(prefix); + if (!strneq8(s, prefix, l)) + return NULL; + + return (char*) s + l; +} + +static bool efi_fnmatch_prefix(const char16_t *p, const char16_t *h, const char16_t **ret_p, const char16_t **ret_h) { + assert(p); + assert(h); + assert(ret_p); + assert(ret_h); + + for (;; p++, h++) + switch (*p) { + case '\0': + /* End of pattern. Check that haystack is now empty. */ + return *h == '\0'; + + case '\\': + p++; + if (*p == '\0' || *p != *h) + /* Trailing escape or no match. */ + return false; + break; + + case '?': + if (*h == '\0') + /* Early end of haystack. */ + return false; + break; + + case '*': + /* Point ret_p at the remainder of the pattern. */ + while (*p == '*') + p++; + *ret_p = p; + *ret_h = h; + return true; + + case '[': + if (*h == '\0') + /* Early end of haystack. */ + return false; + + bool first = true, can_range = true, match = false; + for (;; first = false) { + p++; + if (*p == '\0') + return false; + + if (*p == '\\') { + p++; + if (*p == '\0') + return false; + if (*p == *h) + match = true; + can_range = true; + continue; + } + + /* End of set unless it's the first char. */ + if (*p == ']' && !first) + break; + + /* Range pattern if '-' is not first or last in set. */ + if (*p == '-' && can_range && !first && *(p + 1) != ']') { + char16_t low = *(p - 1); + p++; + if (*p == '\\') + p++; + if (*p == '\0') + return false; + + if (low <= *h && *h <= *p) + match = true; + + /* Ranges cannot be chained: [a-c-f] == [-abcf] */ + can_range = false; + continue; + } + + if (*p == *h) + match = true; + can_range = true; + } + + if (!match) + return false; + break; + + default: + if (*p != *h) + /* Single char mismatch. */ + return false; + } +} + +/* Patterns are fnmatch-compatible (with reduced feature support). */ +bool efi_fnmatch(const char16_t *pattern, const char16_t *haystack) { + /* Patterns can be considered as simple patterns (without '*') concatenated by '*'. By doing so we + * simply have to make sure the very first simple pattern matches the start of haystack. Then we just + * look for the remaining simple patterns *somewhere* within the haystack (in order) as any extra + * characters in between would be matches by the '*'. We then only have to ensure that the very last + * simple pattern matches at the actual end of the haystack. + * + * This means we do not need to use backtracking which could have catastrophic runtimes with the + * right input data. */ + + for (bool first = true;;) { + const char16_t *pattern_tail = NULL, *haystack_tail = NULL; + bool match = efi_fnmatch_prefix(pattern, haystack, &pattern_tail, &haystack_tail); + if (first) { + if (!match) + /* Initial simple pattern must match. */ + return false; + if (!pattern_tail) + /* No '*' was in pattern, we can return early. */ + return true; + first = false; + } + + if (pattern_tail) { + assert(match); + pattern = pattern_tail; + haystack = haystack_tail; + } else { + /* If we have a match this must be at the end of the haystack. Note that + * efi_fnmatch_prefix compares the NUL-bytes at the end, so we cannot match the end + * of pattern in the middle of haystack). */ + if (match || *haystack == '\0') + return match; + + /* Match one character using '*'. */ + haystack++; + } + } +} + +#define DEFINE_PARSE_NUMBER(type, name) \ + bool name(const type *s, uint64_t *ret_u, const type **ret_tail) { \ + assert(ret_u); \ + \ + if (!s) \ + return false; \ + \ + /* Need at least one digit. */ \ + if (*s < '0' || *s > '9') \ + return false; \ + \ + uint64_t u = 0; \ + while (*s >= '0' && *s <= '9') { \ + if (__builtin_mul_overflow(u, 10, &u)) \ + return false; \ + if (__builtin_add_overflow(u, *s - '0', &u)) \ + return false; \ + s++; \ + } \ + \ + if (!ret_tail && *s != '\0') \ + return false; \ + \ + *ret_u = u; \ + if (ret_tail) \ + *ret_tail = s; \ + return true; \ + } + +DEFINE_PARSE_NUMBER(char, parse_number8); +DEFINE_PARSE_NUMBER(char16_t, parse_number16); + +bool parse_boolean(const char *v, bool *ret) { + assert(ret); + + if (!v) + return false; + + if (streq8(v, "1") || streq8(v, "yes") || streq8(v, "y") || streq8(v, "true") || streq8(v, "t") || + streq8(v, "on")) { + *ret = true; + return true; + } + + if (streq8(v, "0") || streq8(v, "no") || streq8(v, "n") || streq8(v, "false") || streq8(v, "f") || + streq8(v, "off")) { + *ret = false; + return true; + } + + return false; +} + +char *line_get_key_value(char *s, const char *sep, size_t *pos, char **ret_key, char **ret_value) { + char *line, *value; + size_t linelen; + + assert(s); + assert(sep); + assert(pos); + assert(ret_key); + assert(ret_value); + + for (;;) { + line = s + *pos; + if (*line == '\0') + return NULL; + + linelen = 0; + while (line[linelen] && !strchr8("\n\r", line[linelen])) + linelen++; + + /* move pos to next line */ + *pos += linelen; + if (s[*pos]) + (*pos)++; + + /* empty line */ + if (linelen == 0) + continue; + + /* terminate line */ + line[linelen] = '\0'; + + /* remove leading whitespace */ + while (linelen > 0 && strchr8(" \t", *line)) { + line++; + linelen--; + } + + /* remove trailing whitespace */ + while (linelen > 0 && strchr8(" \t", line[linelen - 1])) + linelen--; + line[linelen] = '\0'; + + if (*line == '#') + continue; + + /* split key/value */ + value = line; + while (*value && !strchr8(sep, *value)) + value++; + if (*value == '\0') + continue; + *value = '\0'; + value++; + while (*value && strchr8(sep, *value)) + value++; + + /* unquote */ + if (value[0] == '"' && line[linelen - 1] == '"') { + value++; + line[linelen - 1] = '\0'; + } + + *ret_key = line; + *ret_value = value; + return line; + } +} + +char16_t *hexdump(const void *data, size_t size) { + static const char hex[16] = "0123456789abcdef"; + const uint8_t *d = data; + + assert(data || size == 0); + + char16_t *buf = xnew(char16_t, size * 2 + 1); + + for (size_t i = 0; i < size; i++) { + buf[i * 2] = hex[d[i] >> 4]; + buf[i * 2 + 1] = hex[d[i] & 0x0F]; + } + + buf[size * 2] = 0; + return buf; +} + +static const char * const warn_table[] = { + [EFI_SUCCESS] = "Success", + [EFI_WARN_UNKNOWN_GLYPH] = "Unknown glyph", + [EFI_WARN_DELETE_FAILURE] = "Delete failure", + [EFI_WARN_WRITE_FAILURE] = "Write failure", + [EFI_WARN_BUFFER_TOO_SMALL] = "Buffer too small", + [EFI_WARN_STALE_DATA] = "Stale data", + [EFI_WARN_FILE_SYSTEM] = "File system", + [EFI_WARN_RESET_REQUIRED] = "Reset required", +}; + +/* Errors have MSB set, remove it to keep the table compact. */ +#define NOERR(err) ((err) & ~EFI_ERROR_MASK) + +static const char * const err_table[] = { + [NOERR(EFI_ERROR_MASK)] = "Error", + [NOERR(EFI_LOAD_ERROR)] = "Load error", + [NOERR(EFI_INVALID_PARAMETER)] = "Invalid parameter", + [NOERR(EFI_UNSUPPORTED)] = "Unsupported", + [NOERR(EFI_BAD_BUFFER_SIZE)] = "Bad buffer size", + [NOERR(EFI_BUFFER_TOO_SMALL)] = "Buffer too small", + [NOERR(EFI_NOT_READY)] = "Not ready", + [NOERR(EFI_DEVICE_ERROR)] = "Device error", + [NOERR(EFI_WRITE_PROTECTED)] = "Write protected", + [NOERR(EFI_OUT_OF_RESOURCES)] = "Out of resources", + [NOERR(EFI_VOLUME_CORRUPTED)] = "Volume corrupt", + [NOERR(EFI_VOLUME_FULL)] = "Volume full", + [NOERR(EFI_NO_MEDIA)] = "No media", + [NOERR(EFI_MEDIA_CHANGED)] = "Media changed", + [NOERR(EFI_NOT_FOUND)] = "Not found", + [NOERR(EFI_ACCESS_DENIED)] = "Access denied", + [NOERR(EFI_NO_RESPONSE)] = "No response", + [NOERR(EFI_NO_MAPPING)] = "No mapping", + [NOERR(EFI_TIMEOUT)] = "Time out", + [NOERR(EFI_NOT_STARTED)] = "Not started", + [NOERR(EFI_ALREADY_STARTED)] = "Already started", + [NOERR(EFI_ABORTED)] = "Aborted", + [NOERR(EFI_ICMP_ERROR)] = "ICMP error", + [NOERR(EFI_TFTP_ERROR)] = "TFTP error", + [NOERR(EFI_PROTOCOL_ERROR)] = "Protocol error", + [NOERR(EFI_INCOMPATIBLE_VERSION)] = "Incompatible version", + [NOERR(EFI_SECURITY_VIOLATION)] = "Security violation", + [NOERR(EFI_CRC_ERROR)] = "CRC error", + [NOERR(EFI_END_OF_MEDIA)] = "End of media", + [NOERR(EFI_ERROR_RESERVED_29)] = "Reserved (29)", + [NOERR(EFI_ERROR_RESERVED_30)] = "Reserved (30)", + [NOERR(EFI_END_OF_FILE)] = "End of file", + [NOERR(EFI_INVALID_LANGUAGE)] = "Invalid language", + [NOERR(EFI_COMPROMISED_DATA)] = "Compromised data", + [NOERR(EFI_IP_ADDRESS_CONFLICT)] = "IP address conflict", + [NOERR(EFI_HTTP_ERROR)] = "HTTP error", +}; + +static const char *status_to_string(EFI_STATUS status) { + if (status <= ELEMENTSOF(warn_table) - 1) + return warn_table[status]; + if (status >= EFI_ERROR_MASK && status <= ((ELEMENTSOF(err_table) - 1) | EFI_ERROR_MASK)) + return err_table[NOERR(status)]; + return NULL; +} + +typedef struct { + size_t padded_len; /* Field width in printf. */ + size_t len; /* Precision in printf. */ + bool pad_zero; + bool align_left; + bool alternative_form; + bool long_arg; + bool longlong_arg; + bool have_field_width; + + const char *str; + const wchar_t *wstr; + + /* For numbers. */ + bool is_signed; + bool lowercase; + int8_t base; + char sign_pad; /* For + and (space) flags. */ +} SpecifierContext; + +typedef struct { + char16_t stack_buf[128]; /* We use stack_buf first to avoid allocations in most cases. */ + char16_t *dyn_buf; /* Allocated buf or NULL if stack_buf is used. */ + char16_t *buf; /* Points to the current active buf. */ + size_t n_buf; /* Len of buf (in char16_t's, not bytes!). */ + size_t n; /* Used len of buf (in char16_t's). This is always n, need, &need)); + + if (need < ctx->n_buf) + return; + + /* Greedily allocate if we can. */ + if (__builtin_mul_overflow(need, 2, &ctx->n_buf)) + ctx->n_buf = need; + + /* We cannot use realloc here as ctx->buf may be ctx->stack_buf, which we cannot free. */ + char16_t *new_buf = xnew(char16_t, ctx->n_buf); + memcpy(new_buf, ctx->buf, ctx->n * sizeof(*ctx->buf)); + + free(ctx->dyn_buf); + ctx->buf = ctx->dyn_buf = new_buf; +} + +static void push_padding(FormatContext *ctx, char pad, size_t len) { + assert(ctx); + while (len > 0) { + len--; + ctx->buf[ctx->n++] = pad; + } +} + +static bool push_str(FormatContext *ctx, SpecifierContext *sp) { + assert(ctx); + assert(sp); + + sp->padded_len = LESS_BY(sp->padded_len, sp->len); + + grow_buf(ctx, sp->padded_len + sp->len); + + if (!sp->align_left) + push_padding(ctx, ' ', sp->padded_len); + + /* In userspace unit tests we cannot just memcpy() the wide string. */ + if (sp->wstr && sizeof(wchar_t) == sizeof(char16_t)) { + memcpy(ctx->buf + ctx->n, sp->wstr, sp->len * sizeof(*sp->wstr)); + ctx->n += sp->len; + } else { + assert(sp->str || sp->wstr); + for (size_t i = 0; i < sp->len; i++) + ctx->buf[ctx->n++] = sp->str ? sp->str[i] : sp->wstr[i]; + } + + if (sp->align_left) + push_padding(ctx, ' ', sp->padded_len); + + assert(ctx->n < ctx->n_buf); + return true; +} + +static bool push_num(FormatContext *ctx, SpecifierContext *sp, uint64_t u) { + const char *digits = sp->lowercase ? "0123456789abcdef" : "0123456789ABCDEF"; + char16_t tmp[32]; + size_t n = 0; + + assert(ctx); + assert(sp); + assert(IN_SET(sp->base, 10, 16)); + + /* "%.0u" prints nothing if value is 0. */ + if (u == 0 && sp->len == 0) + return true; + + if (sp->is_signed && (int64_t) u < 0) { + /* We cannot just do "u = -(int64_t)u" here because -INT64_MIN overflows. */ + + uint64_t rem = -((int64_t) u % sp->base); + u = (int64_t) u / -sp->base; + tmp[n++] = digits[rem]; + sp->sign_pad = '-'; + } + + while (u > 0 || n == 0) { + uint64_t rem = u % sp->base; + u /= sp->base; + tmp[n++] = digits[rem]; + } + + /* Note that numbers never get truncated! */ + size_t prefix = (sp->sign_pad != 0 ? 1 : 0) + (sp->alternative_form ? 2 : 0); + size_t number_len = prefix + MAX(n, sp->len); + grow_buf(ctx, MAX(sp->padded_len, number_len)); + + size_t padding = 0; + if (sp->pad_zero) + /* Leading zeroes go after the sign or 0x prefix. */ + number_len = MAX(number_len, sp->padded_len); + else + padding = LESS_BY(sp->padded_len, number_len); + + if (!sp->align_left) + push_padding(ctx, ' ', padding); + + if (sp->sign_pad != 0) + ctx->buf[ctx->n++] = sp->sign_pad; + if (sp->alternative_form) { + ctx->buf[ctx->n++] = '0'; + ctx->buf[ctx->n++] = sp->lowercase ? 'x' : 'X'; + } + + push_padding(ctx, '0', LESS_BY(number_len, n + prefix)); + + while (n > 0) + ctx->buf[ctx->n++] = tmp[--n]; + + if (sp->align_left) + push_padding(ctx, ' ', padding); + + assert(ctx->n < ctx->n_buf); + return true; +} + +/* This helps unit testing. */ +#if SD_BOOT +# define NULLSTR "(null)" +# define wcsnlen strnlen16 +#else +# define NULLSTR "(nil)" +#endif + +static bool handle_format_specifier(FormatContext *ctx, SpecifierContext *sp) { + /* Parses one item from the format specifier in ctx and put the info into sp. If we are done with + * this specifier returns true, otherwise this function should be called again. */ + + /* This implementation assumes 32-bit ints. Also note that all types smaller than int are promoted to + * int in vararg functions, which is why we fetch only ints for any such types. The compiler would + * otherwise warn about fetching smaller types. */ + assert_cc(sizeof(int) == 4); + assert_cc(sizeof(wchar_t) <= sizeof(int)); + assert_cc(sizeof(intmax_t) <= sizeof(long long)); + + assert(ctx); + assert(sp); + + switch (*ctx->format) { + case '#': + sp->alternative_form = true; + return false; + case '.': + sp->have_field_width = true; + return false; + case '-': + sp->align_left = true; + return false; + case '+': + case ' ': + sp->sign_pad = *ctx->format; + return false; + + case '0': + if (!sp->have_field_width) { + sp->pad_zero = true; + return false; + } + + /* If field width has already been provided then 0 is part of precision (%.0s). */ + _fallthrough_; + + case '*': + case '1' ... '9': { + int64_t i; + + if (*ctx->format == '*') + i = va_arg(ctx->ap, int); + else { + uint64_t u; + if (!parse_number8(ctx->format, &u, &ctx->format) || u > INT_MAX) + assert_not_reached(); + ctx->format--; /* Point it back to the last digit. */ + i = u; + } + + if (sp->have_field_width) { + /* Negative precision is ignored. */ + if (i >= 0) + sp->len = (size_t) i; + } else { + /* Negative field width is treated as positive field width with '-' flag. */ + if (i < 0) { + i *= -1; + sp->align_left = true; + } + sp->padded_len = i; + } + + return false; + } + + case 'h': + if (*(ctx->format + 1) == 'h') + ctx->format++; + /* char/short gets promoted to int, nothing to do here. */ + return false; + + case 'l': + if (*(ctx->format + 1) == 'l') { + ctx->format++; + sp->longlong_arg = true; + } else + sp->long_arg = true; + return false; + + case 'z': + sp->long_arg = sizeof(size_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(size_t) == sizeof(long long); + return false; + + case 'j': + sp->long_arg = sizeof(intmax_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(intmax_t) == sizeof(long long); + return false; + + case 't': + sp->long_arg = sizeof(ptrdiff_t) == sizeof(long); + sp->longlong_arg = !sp->long_arg && sizeof(ptrdiff_t) == sizeof(long long); + return false; + + case '%': + sp->str = "%"; + sp->len = 1; + return push_str(ctx, sp); + + case 'c': + sp->wstr = &(wchar_t){ va_arg(ctx->ap, int) }; + sp->len = 1; + return push_str(ctx, sp); + + case 's': + if (sp->long_arg) { + sp->wstr = va_arg(ctx->ap, const wchar_t *) ?: L"(null)"; + sp->len = wcsnlen(sp->wstr, sp->len); + } else { + sp->str = va_arg(ctx->ap, const char *) ?: "(null)"; + sp->len = strnlen8(sp->str, sp->len); + } + return push_str(ctx, sp); + + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + sp->lowercase = *ctx->format == 'x'; + sp->is_signed = IN_SET(*ctx->format, 'd', 'i'); + sp->base = IN_SET(*ctx->format, 'x', 'X') ? 16 : 10; + if (sp->len == SIZE_MAX) + sp->len = 1; + + uint64_t v; + if (sp->longlong_arg) + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, long long) : + va_arg(ctx->ap, unsigned long long); + else if (sp->long_arg) + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, long) : va_arg(ctx->ap, unsigned long); + else + v = sp->is_signed ? (uint64_t) va_arg(ctx->ap, int) : va_arg(ctx->ap, unsigned); + + return push_num(ctx, sp, v); + + case 'p': { + const void *ptr = va_arg(ctx->ap, const void *); + if (!ptr) { + sp->str = NULLSTR; + sp->len = STRLEN(NULLSTR); + return push_str(ctx, sp); + } + + sp->base = 16; + sp->lowercase = true; + sp->alternative_form = true; + sp->len = 0; /* Precision is ignored for %p. */ + return push_num(ctx, sp, (uintptr_t) ptr); + } + + case 'm': { + sp->str = status_to_string(ctx->status); + if (sp->str) { + sp->len = strlen8(sp->str); + return push_str(ctx, sp); + } + + sp->base = 16; + sp->lowercase = true; + sp->alternative_form = true; + sp->len = 0; + return push_num(ctx, sp, ctx->status); + } + + default: + assert_not_reached(); + } +} + +/* printf_internal is largely compatible to userspace vasprintf. Any features omitted should trigger asserts. + * + * Supported: + * - Flags: #, 0, +, -, space + * - Lengths: h, hh, l, ll, z, j, t + * - Specifiers: %, c, s, u, i, d, x, X, p, m + * - Precision and width (inline or as int arg using *) + * + * Notable differences: + * - Passing NULL to %s is permitted and will print "(null)" + * - %p will also use "(null)" + * - The provided EFI_STATUS is used for %m instead of errno + * - "\n" is translated to "\r\n" */ +_printf_(2, 0) static char16_t *printf_internal(EFI_STATUS status, const char *format, va_list ap, bool ret) { + assert(format); + + FormatContext ctx = { + .buf = ctx.stack_buf, + .n_buf = ELEMENTSOF(ctx.stack_buf), + .format = format, + .status = status, + }; + + /* We cannot put this into the struct without making a copy. */ + va_copy(ctx.ap, ap); + + while (*ctx.format != '\0') { + SpecifierContext sp = { .len = SIZE_MAX }; + + switch (*ctx.format) { + case '%': + ctx.format++; + while (!handle_format_specifier(&ctx, &sp)) + ctx.format++; + ctx.format++; + break; + case '\n': + ctx.format++; + sp.str = "\r\n"; + sp.len = 2; + push_str(&ctx, &sp); + break; + default: + sp.str = ctx.format++; + while (!IN_SET(*ctx.format, '%', '\n', '\0')) + ctx.format++; + sp.len = ctx.format - sp.str; + push_str(&ctx, &sp); + } + } + + va_end(ctx.ap); + + assert(ctx.n < ctx.n_buf); + ctx.buf[ctx.n++] = '\0'; + + if (ret) { + if (ctx.dyn_buf) + return TAKE_PTR(ctx.dyn_buf); + + char16_t *ret_buf = xnew(char16_t, ctx.n); + memcpy(ret_buf, ctx.buf, ctx.n * sizeof(*ctx.buf)); + return ret_buf; + } + +#if SD_BOOT + ST->ConOut->OutputString(ST->ConOut, ctx.buf); +#endif + + return mfree(ctx.dyn_buf); +} + +void printf_status(EFI_STATUS status, const char *format, ...) { + va_list ap; + va_start(ap, format); + printf_internal(status, format, ap, false); + va_end(ap); +} + +void vprintf_status(EFI_STATUS status, const char *format, va_list ap) { + printf_internal(status, format, ap, false); +} + +char16_t *xasprintf_status(EFI_STATUS status, const char *format, ...) { + va_list ap; + va_start(ap, format); + char16_t *ret = printf_internal(status, format, ap, true); + va_end(ap); + return ret; +} + +char16_t *xvasprintf_status(EFI_STATUS status, const char *format, va_list ap) { + return printf_internal(status, format, ap, true); +} + +#if SD_BOOT +/* To provide the actual implementation for these we need to remove the redirection to the builtins. */ +# undef memchr +# undef memcmp +# undef memcpy +# undef memset +_used_ void *memchr(const void *p, int c, size_t n); +_used_ int memcmp(const void *p1, const void *p2, size_t n); +_used_ void *memcpy(void * restrict dest, const void * restrict src, size_t n); +_used_ void *memset(void *p, int c, size_t n); +#else +/* And for userspace unit testing we need to give them an efi_ prefix. */ +# define memchr efi_memchr +# define memcmp efi_memcmp +# define memcpy efi_memcpy +# define memset efi_memset +#endif + +void *memchr(const void *p, int c, size_t n) { + if (!p || n == 0) + return NULL; + + const uint8_t *q = p; + for (size_t i = 0; i < n; i++) + if (q[i] == (unsigned char) c) + return (void *) (q + i); + + return NULL; +} + +int memcmp(const void *p1, const void *p2, size_t n) { + const uint8_t *up1 = p1, *up2 = p2; + int r; + + if (!p1 || !p2) + return CMP(p1, p2); + + while (n > 0) { + r = CMP(*up1, *up2); + if (r != 0) + return r; + + up1++; + up2++; + n--; + } + + return 0; +} + +void *memcpy(void * restrict dest, const void * restrict src, size_t n) { + if (!dest || !src || n == 0) + return dest; + +#if SD_BOOT + /* The firmware-provided memcpy is likely optimized, so use that. The function is guaranteed to be + * available by the UEFI spec. We still make it depend on the boot services pointer being set just in + * case the compiler emits a call before it is available. */ + if (_likely_(BS)) { + BS->CopyMem(dest, (void *) src, n); + return dest; + } +#endif + + uint8_t *d = dest; + const uint8_t *s = src; + + while (n > 0) { + *d = *s; + d++; + s++; + n--; + } + + return dest; +} + +void *memset(void *p, int c, size_t n) { + if (!p || n == 0) + return p; + +#if SD_BOOT + /* See comment in efi_memcpy. Note that the signature has c and n swapped! */ + if (_likely_(BS)) { + BS->SetMem(p, n, c); + return p; + } +#endif + + uint8_t *q = p; + while (n > 0) { + *q = c; + q++; + n--; + } + + return p; +} diff --git a/src/boot/efi/efi-string.h b/src/boot/efi/efi-string.h new file mode 100644 index 0000000..9ac919f --- /dev/null +++ b/src/boot/efi/efi-string.h @@ -0,0 +1,180 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "macro-fundamental.h" + +size_t strnlen8(const char *s, size_t n); +size_t strnlen16(const char16_t *s, size_t n); + +static inline size_t strlen8(const char *s) { + return strnlen8(s, SIZE_MAX); +} + +static inline size_t strlen16(const char16_t *s) { + return strnlen16(s, SIZE_MAX); +} + +static inline size_t strsize8(const char *s) { + return s ? (strlen8(s) + 1) * sizeof(*s) : 0; +} + +static inline size_t strsize16(const char16_t *s) { + return s ? (strlen16(s) + 1) * sizeof(*s) : 0; +} + +void strtolower8(char *s); +void strtolower16(char16_t *s); + +int strncmp8(const char *s1, const char *s2, size_t n); +int strncmp16(const char16_t *s1, const char16_t *s2, size_t n); +int strncasecmp8(const char *s1, const char *s2, size_t n); +int strncasecmp16(const char16_t *s1, const char16_t *s2, size_t n); + +static inline int strcmp8(const char *s1, const char *s2) { + return strncmp8(s1, s2, SIZE_MAX); +} + +static inline int strcmp16(const char16_t *s1, const char16_t *s2) { + return strncmp16(s1, s2, SIZE_MAX); +} + +static inline int strcasecmp8(const char *s1, const char *s2) { + return strncasecmp8(s1, s2, SIZE_MAX); +} + +static inline int strcasecmp16(const char16_t *s1, const char16_t *s2) { + return strncasecmp16(s1, s2, SIZE_MAX); +} + +static inline bool strneq8(const char *s1, const char *s2, size_t n) { + return strncmp8(s1, s2, n) == 0; +} + +static inline bool strneq16(const char16_t *s1, const char16_t *s2, size_t n) { + return strncmp16(s1, s2, n) == 0; +} + +static inline bool streq8(const char *s1, const char *s2) { + return strcmp8(s1, s2) == 0; +} + +static inline bool streq16(const char16_t *s1, const char16_t *s2) { + return strcmp16(s1, s2) == 0; +} + +static inline int strncaseeq8(const char *s1, const char *s2, size_t n) { + return strncasecmp8(s1, s2, n) == 0; +} + +static inline int strncaseeq16(const char16_t *s1, const char16_t *s2, size_t n) { + return strncasecmp16(s1, s2, n) == 0; +} + +static inline bool strcaseeq8(const char *s1, const char *s2) { + return strcasecmp8(s1, s2) == 0; +} + +static inline bool strcaseeq16(const char16_t *s1, const char16_t *s2) { + return strcasecmp16(s1, s2) == 0; +} + +char *strcpy8(char * restrict dest, const char * restrict src); +char16_t *strcpy16(char16_t * restrict dest, const char16_t * restrict src); + +char *strchr8(const char *s, char c); +char16_t *strchr16(const char16_t *s, char16_t c); + +char *xstrndup8(const char *s, size_t n); +char16_t *xstrndup16(const char16_t *s, size_t n); + +static inline char *xstrdup8(const char *s) { + return xstrndup8(s, SIZE_MAX); +} + +static inline char16_t *xstrdup16(const char16_t *s) { + return xstrndup16(s, SIZE_MAX); +} + +char16_t *xstrn8_to_16(const char *str8, size_t n); +static inline char16_t *xstr8_to_16(const char *str8) { + return xstrn8_to_16(str8, strlen8(str8)); +} + +char *startswith8(const char *s, const char *prefix); + +bool efi_fnmatch(const char16_t *pattern, const char16_t *haystack); + +bool parse_number8(const char *s, uint64_t *ret_u, const char **ret_tail); +bool parse_number16(const char16_t *s, uint64_t *ret_u, const char16_t **ret_tail); + +bool parse_boolean(const char *v, bool *ret); + +char *line_get_key_value(char *s, const char *sep, size_t *pos, char **ret_key, char **ret_value); + +char16_t *hexdump(const void *data, size_t size); + +#ifdef __clang__ +# define _gnu_printf_(a, b) _printf_(a, b) +#else +# define _gnu_printf_(a, b) __attribute__((format(gnu_printf, a, b))) +#endif + +_gnu_printf_(2, 3) void printf_status(EFI_STATUS status, const char *format, ...); +_gnu_printf_(2, 0) void vprintf_status(EFI_STATUS status, const char *format, va_list ap); +_gnu_printf_(2, 3) _warn_unused_result_ char16_t *xasprintf_status(EFI_STATUS status, const char *format, ...); +_gnu_printf_(2, 0) _warn_unused_result_ char16_t *xvasprintf_status(EFI_STATUS status, const char *format, va_list ap); + +#if SD_BOOT +# define printf(...) printf_status(EFI_SUCCESS, __VA_ARGS__) +# define xasprintf(...) xasprintf_status(EFI_SUCCESS, __VA_ARGS__) + +/* inttypes.h is provided by libc instead of the compiler and is not supposed to be used in freestanding + * environments. We could use clang __*_FMT*__ constants for this, bug gcc does not have them. :( */ + +# if defined(__ILP32__) || defined(__arm__) || defined(__i386__) +# define PRI64_PREFIX "ll" +# elif defined(__LP64__) +# define PRI64_PREFIX "l" +# elif defined(__LLP64__) || (__SIZEOF_LONG__ == 4 && __SIZEOF_POINTER__ == 8) +# define PRI64_PREFIX "ll" +# else +# error Unknown 64-bit data model +# endif + +# define PRIi32 "i" +# define PRIu32 "u" +# define PRIx32 "x" +# define PRIX32 "X" +# define PRIiPTR "zi" +# define PRIuPTR "zu" +# define PRIxPTR "zx" +# define PRIXPTR "zX" +# define PRIi64 PRI64_PREFIX "i" +# define PRIu64 PRI64_PREFIX "u" +# define PRIx64 PRI64_PREFIX "x" +# define PRIX64 PRI64_PREFIX "X" + +/* The compiler normally has knowledge about standard functions such as memcmp, but this is not the case when + * compiling with -ffreestanding. By referring to builtins, the compiler can check arguments and do + * optimizations again. Note that we still need to provide implementations as the compiler is free to not + * inline its own implementation and instead issue a library call. */ +# define memchr __builtin_memchr +# define memcmp __builtin_memcmp +# define memcpy __builtin_memcpy +# define memset __builtin_memset + +static inline void *mempcpy(void * restrict dest, const void * restrict src, size_t n) { + if (!dest || !src || n == 0) + return dest; + memcpy(dest, src, n); + return (uint8_t *) dest + n; +} + +#else +/* For unit testing. */ +void *efi_memchr(const void *p, int c, size_t n); +int efi_memcmp(const void *p1, const void *p2, size_t n); +void *efi_memcpy(void * restrict dest, const void * restrict src, size_t n); +void *efi_memset(void *p, int c, size_t n); +#endif diff --git a/src/boot/efi/efi.h b/src/boot/efi/efi.h new file mode 100644 index 0000000..fbc5d10 --- /dev/null +++ b/src/boot/efi/efi.h @@ -0,0 +1,459 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include +#include +#include + +#include "macro-fundamental.h" + +#if SD_BOOT +/* uchar.h/wchar.h are not suitable for freestanding environments. */ +typedef __WCHAR_TYPE__ wchar_t; +typedef __CHAR16_TYPE__ char16_t; +typedef __CHAR32_TYPE__ char32_t; + +/* Let's be paranoid and do some sanity checks. */ +assert_cc(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__); +assert_cc(__STDC_HOSTED__ == 0); +assert_cc(sizeof(bool) == 1); +assert_cc(sizeof(uint8_t) == 1); +assert_cc(sizeof(uint16_t) == 2); +assert_cc(sizeof(uint32_t) == 4); +assert_cc(sizeof(uint64_t) == 8); +assert_cc(sizeof(wchar_t) == 2); +assert_cc(sizeof(char16_t) == 2); +assert_cc(sizeof(char32_t) == 4); +assert_cc(sizeof(size_t) == sizeof(void *)); +assert_cc(sizeof(size_t) == sizeof(uintptr_t)); +assert_cc(alignof(bool) == 1); +assert_cc(alignof(uint8_t) == 1); +assert_cc(alignof(uint16_t) == 2); +assert_cc(alignof(uint32_t) == 4); +assert_cc(alignof(uint64_t) == 8); +assert_cc(alignof(wchar_t) == 2); +assert_cc(alignof(char16_t) == 2); +assert_cc(alignof(char32_t) == 4); + +# if defined(__x86_64__) && defined(__ILP32__) +# error Building for x64 requires -m64 on x32 ABI. +# endif +#else +# include +# include +#endif + +/* We use size_t/ssize_t to represent UEFI UINTN/INTN. */ +typedef size_t EFI_STATUS; +typedef intptr_t ssize_t; + +typedef void* EFI_HANDLE; +typedef void* EFI_EVENT; +typedef size_t EFI_TPL; +typedef uint64_t EFI_LBA; +typedef uint64_t EFI_PHYSICAL_ADDRESS; + +#if defined(__x86_64__) && !defined(__ILP32__) +# define EFIAPI __attribute__((ms_abi)) +#else +# define EFIAPI +#endif + +#if __SIZEOF_POINTER__ == 8 +# define EFI_ERROR_MASK 0x8000000000000000ULL +#elif __SIZEOF_POINTER__ == 4 +# define EFI_ERROR_MASK 0x80000000ULL +#else +# error Unsupported pointer size +#endif + +#define EFIWARN(s) ((EFI_STATUS) s) +#define EFIERR(s) ((EFI_STATUS) (s | EFI_ERROR_MASK)) + +#define EFI_SUCCESS EFIWARN(0) +#define EFI_WARN_UNKNOWN_GLYPH EFIWARN(1) +#define EFI_WARN_DELETE_FAILURE EFIWARN(2) +#define EFI_WARN_WRITE_FAILURE EFIWARN(3) +#define EFI_WARN_BUFFER_TOO_SMALL EFIWARN(4) +#define EFI_WARN_STALE_DATA EFIWARN(5) +#define EFI_WARN_FILE_SYSTEM EFIWARN(6) +#define EFI_WARN_RESET_REQUIRED EFIWARN(7) + +#define EFI_LOAD_ERROR EFIERR(1) +#define EFI_INVALID_PARAMETER EFIERR(2) +#define EFI_UNSUPPORTED EFIERR(3) +#define EFI_BAD_BUFFER_SIZE EFIERR(4) +#define EFI_BUFFER_TOO_SMALL EFIERR(5) +#define EFI_NOT_READY EFIERR(6) +#define EFI_DEVICE_ERROR EFIERR(7) +#define EFI_WRITE_PROTECTED EFIERR(8) +#define EFI_OUT_OF_RESOURCES EFIERR(9) +#define EFI_VOLUME_CORRUPTED EFIERR(10) +#define EFI_VOLUME_FULL EFIERR(11) +#define EFI_NO_MEDIA EFIERR(12) +#define EFI_MEDIA_CHANGED EFIERR(13) +#define EFI_NOT_FOUND EFIERR(14) +#define EFI_ACCESS_DENIED EFIERR(15) +#define EFI_NO_RESPONSE EFIERR(16) +#define EFI_NO_MAPPING EFIERR(17) +#define EFI_TIMEOUT EFIERR(18) +#define EFI_NOT_STARTED EFIERR(19) +#define EFI_ALREADY_STARTED EFIERR(20) +#define EFI_ABORTED EFIERR(21) +#define EFI_ICMP_ERROR EFIERR(22) +#define EFI_TFTP_ERROR EFIERR(23) +#define EFI_PROTOCOL_ERROR EFIERR(24) +#define EFI_INCOMPATIBLE_VERSION EFIERR(25) +#define EFI_SECURITY_VIOLATION EFIERR(26) +#define EFI_CRC_ERROR EFIERR(27) +#define EFI_END_OF_MEDIA EFIERR(28) +#define EFI_ERROR_RESERVED_29 EFIERR(29) +#define EFI_ERROR_RESERVED_30 EFIERR(30) +#define EFI_END_OF_FILE EFIERR(31) +#define EFI_INVALID_LANGUAGE EFIERR(32) +#define EFI_COMPROMISED_DATA EFIERR(33) +#define EFI_IP_ADDRESS_CONFLICT EFIERR(34) +#define EFI_HTTP_ERROR EFIERR(35) + +typedef struct { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; +} EFI_GUID; + +#define GUID_DEF(d1, d2, d3, d4_1, d4_2, d4_3, d4_4, d4_5, d4_6, d4_7, d4_8) \ + { d1, d2, d3, { d4_1, d4_2, d4_3, d4_4, d4_5, d4_6, d4_7, d4_8 } } + +/* Creates a EFI_GUID pointer suitable for EFI APIs. Use of const allows the compiler to merge multiple + * uses (although, currently compilers do that regardless). Most EFI APIs declare their EFI_GUID input + * as non-const, but almost all of them are in fact const. */ +#define MAKE_GUID_PTR(name) ((EFI_GUID *) &(const EFI_GUID) name##_GUID) + +/* These allow MAKE_GUID_PTR() to work without requiring an extra _GUID in the passed name. We want to + * keep the GUID definitions in line with the UEFI spec. */ +#define EFI_GLOBAL_VARIABLE_GUID EFI_GLOBAL_VARIABLE +#define EFI_FILE_INFO_GUID EFI_FILE_INFO_ID + +#define EFI_GLOBAL_VARIABLE \ + GUID_DEF(0x8be4df61, 0x93ca, 0x11d2, 0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c) +#define EFI_IMAGE_SECURITY_DATABASE_GUID \ + GUID_DEF(0xd719b2cb, 0x3d3a, 0x4596, 0xa3, 0xbc, 0xda, 0xd0, 0x0e, 0x67, 0x65, 0x6f) + +#define EVT_TIMER 0x80000000U +#define EVT_RUNTIME 0x40000000U +#define EVT_NOTIFY_WAIT 0x00000100U +#define EVT_NOTIFY_SIGNAL 0x00000200U +#define EVT_SIGNAL_EXIT_BOOT_SERVICES 0x00000201U +#define EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE 0x60000202U + +#define EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL 0x01U +#define EFI_OPEN_PROTOCOL_GET_PROTOCOL 0x02U +#define EFI_OPEN_PROTOCOL_TEST_PROTOCOL 0x04U +#define EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER 0x08U +#define EFI_OPEN_PROTOCOL_BY_DRIVER 0x10U +#define EFI_OPEN_PROTOCOL_EXCLUSIVE 0x20U + +#define EFI_VARIABLE_NON_VOLATILE 0x01U +#define EFI_VARIABLE_BOOTSERVICE_ACCESS 0x02U +#define EFI_VARIABLE_RUNTIME_ACCESS 0x04U +#define EFI_VARIABLE_HARDWARE_ERROR_RECORD 0x08U +#define EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS 0x10U +#define EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS 0x20U +#define EFI_VARIABLE_APPEND_WRITE 0x40U +#define EFI_VARIABLE_ENHANCED_AUTHENTICATED_ACCESS 0x80U + +#define EFI_TIME_ADJUST_DAYLIGHT 0x001U +#define EFI_TIME_IN_DAYLIGHT 0x002U +#define EFI_UNSPECIFIED_TIMEZONE 0x7FFU + +#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x01U +#define EFI_OS_INDICATIONS_TIMESTAMP_REVOCATION 0x02U +#define EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED 0x04U +#define EFI_OS_INDICATIONS_FMP_CAPSULE_SUPPORTED 0x08U +#define EFI_OS_INDICATIONS_CAPSULE_RESULT_VAR_SUPPORTED 0x10U +#define EFI_OS_INDICATIONS_START_OS_RECOVERY 0x20U +#define EFI_OS_INDICATIONS_START_PLATFORM_RECOVERY 0x40U +#define EFI_OS_INDICATIONS_JSON_CONFIG_DATA_REFRESH 0x80U + +#define EFI_PAGE_SIZE 4096U +#define EFI_SIZE_TO_PAGES(s) (((s) + 0xFFFU) >> 12U) + +/* These are common enough to warrant forward declaration. We also give them a + * shorter name for convenience. */ +typedef struct EFI_FILE_PROTOCOL EFI_FILE; +typedef struct EFI_DEVICE_PATH_PROTOCOL EFI_DEVICE_PATH; + +typedef struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL EFI_SIMPLE_TEXT_INPUT_PROTOCOL; +typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; + +typedef enum { + TimerCancel, + TimerPeriodic, + TimerRelative, +} EFI_TIMER_DELAY; + +typedef enum { + AllocateAnyPages, + AllocateMaxAddress, + AllocateAddress, + MaxAllocateType, +} EFI_ALLOCATE_TYPE; + +typedef enum { + EfiReservedMemoryType, + EfiLoaderCode, + EfiLoaderData, + EfiBootServicesCode, + EfiBootServicesData, + EfiRuntimeServicesCode, + EfiRuntimeServicesData, + EfiConventionalMemory, + EfiUnusableMemory, + EfiACPIReclaimMemory, + EfiACPIMemoryNVS, + EfiMemoryMappedIO, + EfiMemoryMappedIOPortSpace, + EfiPalCode, + EfiPersistentMemory, + EfiUnacceptedMemoryType, + EfiMaxMemoryType, +} EFI_MEMORY_TYPE; + +typedef enum { + AllHandles, + ByRegisterNotify, + ByProtocol, +} EFI_LOCATE_SEARCH_TYPE; + +typedef enum { + EfiResetCold, + EfiResetWarm, + EfiResetShutdown, + EfiResetPlatformSpecific, +} EFI_RESET_TYPE; + +typedef struct { + uint16_t Year; + uint8_t Month; + uint8_t Day; + uint8_t Hour; + uint8_t Minute; + uint8_t Second; + uint8_t Pad1; + uint32_t Nanosecond; + int16_t TimeZone; + uint8_t Daylight; + uint8_t Pad2; +} EFI_TIME; + +typedef struct { + uint32_t Resolution; + uint32_t Accuracy; + bool SetsToZero; +} EFI_TIME_CAPABILITIES; + +typedef struct { + uint64_t Signature; + uint32_t Revision; + uint32_t HeaderSize; + uint32_t CRC32; + uint32_t Reserved; +} EFI_TABLE_HEADER; + +typedef struct { + EFI_TABLE_HEADER Hdr; + void *RaiseTPL; + void *RestoreTPL; + EFI_STATUS (EFIAPI *AllocatePages)( + EFI_ALLOCATE_TYPE Type, + EFI_MEMORY_TYPE MemoryType, + size_t Pages, + EFI_PHYSICAL_ADDRESS *Memory); + EFI_STATUS (EFIAPI *FreePages)( + EFI_PHYSICAL_ADDRESS Memory, + size_t Pages); + void *GetMemoryMap; + EFI_STATUS (EFIAPI *AllocatePool)( + EFI_MEMORY_TYPE PoolType, + size_t Size, + void **Buffer); + EFI_STATUS (EFIAPI *FreePool)(void *Buffer); + EFI_STATUS (EFIAPI *CreateEvent)( + uint32_t Type, + EFI_TPL NotifyTpl, + void *NotifyFunction, + void *NotifyContext, + EFI_EVENT *Event); + EFI_STATUS (EFIAPI *SetTimer)( + EFI_EVENT Event, + EFI_TIMER_DELAY Type, + uint64_t TriggerTime); + EFI_STATUS (EFIAPI *WaitForEvent)( + size_t NumberOfEvents, + EFI_EVENT *Event, + size_t *Index); + void *SignalEvent; + EFI_STATUS (EFIAPI *CloseEvent)(EFI_EVENT Event); + EFI_STATUS (EFIAPI *CheckEvent)(EFI_EVENT Event); + void *InstallProtocolInterface; + EFI_STATUS (EFIAPI *ReinstallProtocolInterface)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void *OldInterface, + void *NewInterface); + void *UninstallProtocolInterface; + EFI_STATUS (EFIAPI *HandleProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void **Interface); + void *Reserved; + void *RegisterProtocolNotify; + EFI_STATUS (EFIAPI *LocateHandle)( + EFI_LOCATE_SEARCH_TYPE SearchType, + EFI_GUID *Protocol, + void *SearchKey, + size_t *BufferSize, + EFI_HANDLE *Buffer); + EFI_STATUS (EFIAPI *LocateDevicePath)( + EFI_GUID *Protocol, + EFI_DEVICE_PATH **DevicePath, + EFI_HANDLE *Device); + EFI_STATUS (EFIAPI *InstallConfigurationTable)( + EFI_GUID *Guid, + void *Table); + EFI_STATUS (EFIAPI *LoadImage)( + bool BootPolicy, + EFI_HANDLE ParentImageHandle, + EFI_DEVICE_PATH *DevicePath, + void *SourceBuffer, + size_t SourceSize, + EFI_HANDLE *ImageHandle); + EFI_STATUS (EFIAPI *StartImage)( + EFI_HANDLE ImageHandle, + size_t *ExitDataSize, + char16_t **ExitData); + EFI_STATUS (EFIAPI *Exit)( + EFI_HANDLE ImageHandle, + EFI_STATUS ExitStatus, + size_t ExitDataSize, + char16_t *ExitData); + EFI_STATUS (EFIAPI *UnloadImage)(EFI_HANDLE ImageHandle); + void *ExitBootServices; + EFI_STATUS (EFIAPI *GetNextMonotonicCount)(uint64_t *Count); + EFI_STATUS (EFIAPI *Stall)(size_t Microseconds); + EFI_STATUS (EFIAPI *SetWatchdogTimer)( + size_t Timeout, + uint64_t WatchdogCode, + size_t DataSize, + char16_t *WatchdogData); + EFI_STATUS (EFIAPI *ConnectController)( + EFI_HANDLE ControllerHandle, + EFI_HANDLE *DriverImageHandle, + EFI_DEVICE_PATH *RemainingDevicePath, + bool Recursive); + EFI_STATUS (EFIAPI *DisconnectController)( + EFI_HANDLE ControllerHandle, + EFI_HANDLE DriverImageHandle, + EFI_HANDLE ChildHandle); + EFI_STATUS (EFIAPI *OpenProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + void **Interface, + EFI_HANDLE AgentHandle, + EFI_HANDLE ControllerHandle, + uint32_t Attributes); + EFI_STATUS (EFIAPI *CloseProtocol)( + EFI_HANDLE Handle, + EFI_GUID *Protocol, + EFI_HANDLE AgentHandle, + EFI_HANDLE ControllerHandle); + void *OpenProtocolInformation; + EFI_STATUS (EFIAPI *ProtocolsPerHandle)( + EFI_HANDLE Handle, + EFI_GUID ***ProtocolBuffer, + size_t *ProtocolBufferCount); + EFI_STATUS (EFIAPI *LocateHandleBuffer)( + EFI_LOCATE_SEARCH_TYPE SearchType, + EFI_GUID *Protocol, + void *SearchKey, + size_t *NoHandles, + EFI_HANDLE **Buffer); + EFI_STATUS (EFIAPI *LocateProtocol)( + EFI_GUID *Protocol, + void *Registration, + void **Interface); + EFI_STATUS (EFIAPI *InstallMultipleProtocolInterfaces)(EFI_HANDLE *Handle, ...); + EFI_STATUS (EFIAPI *UninstallMultipleProtocolInterfaces)(EFI_HANDLE Handle, ...); + EFI_STATUS (EFIAPI *CalculateCrc32)( + void *Data, + size_t DataSize, + uint32_t *Crc32); + void (EFIAPI *CopyMem)( + void *Destination, + void *Source, + size_t Length); + void (EFIAPI *SetMem)( + void *Buffer, + size_t Size, + uint8_t Value); + void *CreateEventEx; +} EFI_BOOT_SERVICES; + +typedef struct { + EFI_TABLE_HEADER Hdr; + EFI_STATUS (EFIAPI *GetTime)( + EFI_TIME *Time, + EFI_TIME_CAPABILITIES *Capabilities); + EFI_STATUS (EFIAPI *SetTime)(EFI_TIME *Time); + void *GetWakeupTime; + void *SetWakeupTime; + void *SetVirtualAddressMap; + void *ConvertPointer; + EFI_STATUS (EFIAPI *GetVariable)( + char16_t *VariableName, + EFI_GUID *VendorGuid, + uint32_t *Attributes, + size_t *DataSize, + void *Data); + void *GetNextVariableName; + EFI_STATUS (EFIAPI *SetVariable)( + char16_t *VariableName, + EFI_GUID *VendorGuid, + uint32_t Attributes, + size_t DataSize, + void *Data); + EFI_STATUS (EFIAPI *GetNextHighMonotonicCount)(uint32_t *HighCount); + void (EFIAPI *ResetSystem)( + EFI_RESET_TYPE ResetType, + EFI_STATUS ResetStatus, + size_t DataSize, + void *ResetData); + void *UpdateCapsule; + void *QueryCapsuleCapabilities; + void *QueryVariableInfo; +} EFI_RUNTIME_SERVICES; + +typedef struct { + EFI_TABLE_HEADER Hdr; + char16_t *FirmwareVendor; + uint32_t FirmwareRevision; + EFI_HANDLE ConsoleInHandle; + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; + EFI_HANDLE ConsoleOutHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; + EFI_HANDLE StandardErrorHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; + EFI_RUNTIME_SERVICES *RuntimeServices; + EFI_BOOT_SERVICES *BootServices; + size_t NumberOfTableEntries; + struct { + EFI_GUID VendorGuid; + void *VendorTable; + } *ConfigurationTable; +} EFI_SYSTEM_TABLE; + +extern EFI_SYSTEM_TABLE *ST; +extern EFI_BOOT_SERVICES *BS; +extern EFI_RUNTIME_SERVICES *RT; diff --git a/src/boot/efi/fuzz-bcd.c b/src/boot/efi/fuzz-bcd.c new file mode 100644 index 0000000..cb5be7a --- /dev/null +++ b/src/boot/efi/fuzz-bcd.c @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bcd.h" +#include "fuzz.h" +#include "utf8.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_free_ void *p = NULL; + + /* This limit was borrowed from src/boot/efi/boot.c */ + if (outside_size_range(size, 0, 100*1024)) + return 0; + + fuzz_setup_logging(); + + p = memdup(data, size); + assert_se(p); + + char16_t *title = get_bcd_title(p, size); + /* If we get something, it must be NUL-terminated, but an empty string is still valid! */ + DO_NOT_OPTIMIZE(title && char16_strlen(title)); + return 0; +} diff --git a/src/boot/efi/fuzz-efi-osrel.c b/src/boot/efi/fuzz-efi-osrel.c new file mode 100644 index 0000000..1a5a9bc --- /dev/null +++ b/src/boot/efi/fuzz-efi-osrel.c @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "efi-string.h" +#include "fuzz.h" + +#define SEP_LEN 4 + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (outside_size_range(size, SEP_LEN + 1, 64 * 1024)) + return 0; + if (data[SEP_LEN] != '\0') + return 0; + + fuzz_setup_logging(); + + _cleanup_free_ char *p = memdup_suffix0(data + SEP_LEN + 1, size - SEP_LEN - 1); + assert_se(p); + + size_t pos = 0; + char *key, *value; + while (line_get_key_value(p, (const char *) data, &pos, &key, &value)) { + assert_se(key); + assert_se(value); + } + + return 0; +} diff --git a/src/boot/efi/fuzz-efi-printf.c b/src/boot/efi/fuzz-efi-printf.c new file mode 100644 index 0000000..6dee830 --- /dev/null +++ b/src/boot/efi/fuzz-efi-printf.c @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "efi-string.h" +#include "fuzz.h" +#include "utf8.h" + +typedef struct { + EFI_STATUS status; + int16_t field_width; + int16_t precision; + const void *ptr; + char c; + unsigned char uchar; + signed char schar; + unsigned short ushort; + signed short sshort; + unsigned int uint; + signed int sint; + unsigned long ulong; + signed long slong; + unsigned long long ulonglong; + signed long long slonglong; + size_t size; + ssize_t ssize; + intmax_t intmax; + uintmax_t uintmax; + ptrdiff_t ptrdiff; + char str[]; +} Input; + +#define PRINTF_ONE(...) \ + ({ \ + _cleanup_free_ char16_t *_ret = xasprintf_status(__VA_ARGS__); \ + DO_NOT_OPTIMIZE(_ret); \ + }) + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (outside_size_range(size, sizeof(Input), 1024 * 1024)) + return 0; + + const Input *i = (const Input *) data; + size_t len = size - offsetof(Input, str); + + fuzz_setup_logging(); + + PRINTF_ONE(i->status, "%*.*s", i->field_width, (int) len, i->str); + PRINTF_ONE(i->status, "%*.*ls", i->field_width, (int) (len / sizeof(wchar_t)), (const wchar_t *) i->str); + + PRINTF_ONE(i->status, "%% %*.*m", i->field_width, i->precision); + PRINTF_ONE(i->status, "%*p", i->field_width, i->ptr); + PRINTF_ONE(i->status, "%*c %12340c %56789c", i->field_width, i->c, i->c, i->c); + + PRINTF_ONE(i->status, "%*.*hhu", i->field_width, i->precision, i->uchar); + PRINTF_ONE(i->status, "%*.*hhi", i->field_width, i->precision, i->schar); + PRINTF_ONE(i->status, "%*.*hu", i->field_width, i->precision, i->ushort); + PRINTF_ONE(i->status, "%*.*hi", i->field_width, i->precision, i->sshort); + PRINTF_ONE(i->status, "%*.*u", i->field_width, i->precision, i->uint); + PRINTF_ONE(i->status, "%*.*i", i->field_width, i->precision, i->sint); + PRINTF_ONE(i->status, "%*.*lu", i->field_width, i->precision, i->ulong); + PRINTF_ONE(i->status, "%*.*li", i->field_width, i->precision, i->slong); + PRINTF_ONE(i->status, "%*.*llu", i->field_width, i->precision, i->ulonglong); + PRINTF_ONE(i->status, "%*.*lli", i->field_width, i->precision, i->slonglong); + + PRINTF_ONE(i->status, "%+*.*hhi", i->field_width, i->precision, i->schar); + PRINTF_ONE(i->status, "%-*.*hi", i->field_width, i->precision, i->sshort); + PRINTF_ONE(i->status, "% *.*i", i->field_width, i->precision, i->sint); + PRINTF_ONE(i->status, "%0*li", i->field_width, i->slong); + PRINTF_ONE(i->status, "%#*.*llx", i->field_width, i->precision, i->ulonglong); + + PRINTF_ONE(i->status, "%-*.*zx", i->field_width, i->precision, i->size); + PRINTF_ONE(i->status, "% *.*zi", i->field_width, i->precision, i->ssize); + PRINTF_ONE(i->status, "%0*ji", i->field_width, i->intmax); + PRINTF_ONE(i->status, "%#0*jX", i->field_width, i->uintmax); + PRINTF_ONE(i->status, "%*.*ti", i->field_width, i->precision, i->ptrdiff); + + return 0; +} diff --git a/src/boot/efi/fuzz-efi-string.c b/src/boot/efi/fuzz-efi-string.c new file mode 100644 index 0000000..36ecaf9 --- /dev/null +++ b/src/boot/efi/fuzz-efi-string.c @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "efi-string.h" +#include "fuzz.h" +#include "utf8.h" + +static char16_t *memdup_str16(const uint8_t *data, size_t size) { + char16_t *ret = memdup(data, size); + assert_se(ret); + ret[size / sizeof(char16_t) - 1] = '\0'; + return ret; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (outside_size_range(size, sizeof(size_t), 64 * 1024)) + return 0; + + fuzz_setup_logging(); + + size_t len, len2; + memcpy(&len, data, sizeof(len)); + data += sizeof(len); + size -= sizeof(len); + + len2 = size - len; + if (len > size || len < sizeof(char16_t) || len2 < sizeof(char16_t)) + return 0; + + const char *tail8 = NULL; + _cleanup_free_ char *str8 = ASSERT_SE_PTR(memdup_suffix0(data, size)); + DO_NOT_OPTIMIZE(parse_number8(str8, &(uint64_t){ 0 }, size % 2 == 0 ? NULL : &tail8)); + + const char16_t *tail16 = NULL; + _cleanup_free_ char16_t *str16 = memdup_str16(data, size); + DO_NOT_OPTIMIZE(parse_number16(str16, &(uint64_t){ 0 }, size % 2 == 0 ? NULL : &tail16)); + + _cleanup_free_ char16_t *pattern = memdup_str16(data, len), *haystack = memdup_str16(data + len, len2); + DO_NOT_OPTIMIZE(efi_fnmatch(pattern, haystack)); + + return 0; +} diff --git a/src/boot/efi/graphics.c b/src/boot/efi/graphics.c new file mode 100644 index 0000000..496fc69 --- /dev/null +++ b/src/boot/efi/graphics.c @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright © 2013 Intel Corporation + * Authored by Joonas Lahtinen + */ + +#include "graphics.h" +#include "proto/console-control.h" +#include "proto/simple-text-io.h" +#include "util.h" + +EFI_STATUS graphics_mode(bool on) { + EFI_CONSOLE_CONTROL_PROTOCOL *ConsoleControl = NULL; + EFI_CONSOLE_CONTROL_SCREEN_MODE new; + EFI_CONSOLE_CONTROL_SCREEN_MODE current; + bool uga_exists, stdin_locked; + EFI_STATUS err; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_CONSOLE_CONTROL_PROTOCOL), NULL, (void **) &ConsoleControl); + if (err != EFI_SUCCESS) + /* console control protocol is nonstandard and might not exist. */ + return err == EFI_NOT_FOUND ? EFI_SUCCESS : err; + + /* check current mode */ + err = ConsoleControl->GetMode(ConsoleControl, ¤t, &uga_exists, &stdin_locked); + if (err != EFI_SUCCESS) + return err; + + /* do not touch the mode */ + new = on ? EfiConsoleControlScreenGraphics : EfiConsoleControlScreenText; + if (new == current) + return EFI_SUCCESS; + + log_wait(); + err = ConsoleControl->SetMode(ConsoleControl, new); + + /* some firmware enables the cursor when switching modes */ + ST->ConOut->EnableCursor(ST->ConOut, false); + + return err; +} diff --git a/src/boot/efi/graphics.h b/src/boot/efi/graphics.h new file mode 100644 index 0000000..33ab7f8 --- /dev/null +++ b/src/boot/efi/graphics.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright © 2013 Intel Corporation + * Authored by Joonas Lahtinen + */ +#pragma once + +#include "efi.h" + +EFI_STATUS graphics_mode(bool on); diff --git a/src/boot/efi/initrd.c b/src/boot/efi/initrd.c new file mode 100644 index 0000000..527b05f --- /dev/null +++ b/src/boot/efi/initrd.c @@ -0,0 +1,136 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "initrd.h" +#include "macro-fundamental.h" +#include "proto/device-path.h" +#include "proto/load-file.h" +#include "util.h" + +#define LINUX_INITRD_MEDIA_GUID \ + GUID_DEF(0x5568e427, 0x68fc, 0x4f3d, 0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68) + +/* extend LoadFileProtocol */ +struct initrd_loader { + EFI_LOAD_FILE_PROTOCOL load_file; + const void *address; + size_t length; +}; + +/* static structure for LINUX_INITRD_MEDIA device path + see https://github.com/torvalds/linux/blob/v5.13/drivers/firmware/efi/libstub/efi-stub-helper.c + */ +static const struct { + VENDOR_DEVICE_PATH vendor; + EFI_DEVICE_PATH end; +} _packed_ efi_initrd_device_path = { + .vendor = { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_VENDOR_DP, + .Length = sizeof(efi_initrd_device_path.vendor), + }, + .Guid = LINUX_INITRD_MEDIA_GUID + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = sizeof(efi_initrd_device_path.end), + } +}; + +static EFIAPI EFI_STATUS initrd_load_file( + EFI_LOAD_FILE_PROTOCOL *this, + EFI_DEVICE_PATH *file_path, + bool boot_policy, + size_t *buffer_size, + void *buffer) { + + struct initrd_loader *loader; + + if (!this || !buffer_size || !file_path) + return EFI_INVALID_PARAMETER; + if (boot_policy) + return EFI_UNSUPPORTED; + + loader = (struct initrd_loader *) this; + + if (loader->length == 0 || !loader->address) + return EFI_NOT_FOUND; + + if (!buffer || *buffer_size < loader->length) { + *buffer_size = loader->length; + return EFI_BUFFER_TOO_SMALL; + } + + memcpy(buffer, loader->address, loader->length); + *buffer_size = loader->length; + return EFI_SUCCESS; +} + +EFI_STATUS initrd_register( + const void *initrd_address, + size_t initrd_length, + EFI_HANDLE *ret_initrd_handle) { + + EFI_STATUS err; + EFI_DEVICE_PATH *dp = (EFI_DEVICE_PATH *) &efi_initrd_device_path; + EFI_HANDLE handle; + struct initrd_loader *loader; + + assert(ret_initrd_handle); + + if (!initrd_address || initrd_length == 0) + return EFI_SUCCESS; + + /* check if a LINUX_INITRD_MEDIA_GUID DevicePath is already registered. + LocateDevicePath checks for the "closest DevicePath" and returns its handle, + where as InstallMultipleProtocolInterfaces only matches identical DevicePaths. + */ + err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), &dp, &handle); + if (err != EFI_NOT_FOUND) /* InitrdMedia is already registered */ + return EFI_ALREADY_STARTED; + + loader = xnew(struct initrd_loader, 1); + *loader = (struct initrd_loader) { + .load_file.LoadFile = initrd_load_file, + .address = initrd_address, + .length = initrd_length + }; + + /* create a new handle and register the LoadFile2 protocol with the InitrdMediaPath on it */ + err = BS->InstallMultipleProtocolInterfaces( + ret_initrd_handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), + &efi_initrd_device_path, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), + loader, + NULL); + if (err != EFI_SUCCESS) + free(loader); + + return err; +} + +EFI_STATUS initrd_unregister(EFI_HANDLE initrd_handle) { + EFI_STATUS err; + struct initrd_loader *loader; + + if (!initrd_handle) + return EFI_SUCCESS; + + /* get the LoadFile2 protocol that we allocated earlier */ + err = BS->HandleProtocol(initrd_handle, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), (void **) &loader); + if (err != EFI_SUCCESS) + return err; + + /* uninstall all protocols thus destroying the handle */ + err = BS->UninstallMultipleProtocolInterfaces( + initrd_handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), + &efi_initrd_device_path, MAKE_GUID_PTR(EFI_LOAD_FILE2_PROTOCOL), + loader, + NULL); + if (err != EFI_SUCCESS) + return err; + + initrd_handle = NULL; + free(loader); + return EFI_SUCCESS; +} diff --git a/src/boot/efi/initrd.h b/src/boot/efi/initrd.h new file mode 100644 index 0000000..e7685ae --- /dev/null +++ b/src/boot/efi/initrd.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS initrd_register( + const void *initrd_address, + size_t initrd_length, + EFI_HANDLE *ret_initrd_handle); + +EFI_STATUS initrd_unregister(EFI_HANDLE initrd_handle); + +static inline void cleanup_initrd(EFI_HANDLE *initrd_handle) { + (void) initrd_unregister(*initrd_handle); + *initrd_handle = NULL; +} diff --git a/src/boot/efi/linux.c b/src/boot/efi/linux.c new file mode 100644 index 0000000..65bc176 --- /dev/null +++ b/src/boot/efi/linux.c @@ -0,0 +1,156 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * Generic Linux boot protocol using the EFI/PE entry point of the kernel. Passes + * initrd with the LINUX_INITRD_MEDIA_GUID DevicePath and cmdline with + * EFI LoadedImageProtocol. + * + * This method works for Linux 5.8 and newer on ARM/Aarch64, x86/x68_64 and RISC-V. + */ + +#include "initrd.h" +#include "linux.h" +#include "pe.h" +#include "proto/device-path.h" +#include "proto/loaded-image.h" +#include "secure-boot.h" +#include "util.h" + +#define STUB_PAYLOAD_GUID \ + { 0x55c5d1f8, 0x04cd, 0x46b5, { 0x8a, 0x20, 0xe5, 0x6c, 0xbb, 0x30, 0x52, 0xd0 } } + +typedef struct { + const void *addr; + size_t len; + const EFI_DEVICE_PATH *device_path; +} ValidationContext; + +static bool validate_payload( + const void *ctx, const EFI_DEVICE_PATH *device_path, const void *file_buffer, size_t file_size) { + + const ValidationContext *payload = ASSERT_PTR(ctx); + + if (device_path != payload->device_path) + return false; + + /* Security arch (1) protocol does not provide a file buffer. Instead we are supposed to fetch the payload + * ourselves, which is not needed as we already have everything in memory and the device paths match. */ + if (file_buffer && (file_buffer != payload->addr || file_size != payload->len)) + return false; + + return true; +} + +static EFI_STATUS load_image(EFI_HANDLE parent, const void *source, size_t len, EFI_HANDLE *ret_image) { + assert(parent); + assert(source); + assert(ret_image); + + /* We could pass a NULL device path, but it's nicer to provide something and it allows us to identify + * the loaded image from within the security hooks. */ + struct { + VENDOR_DEVICE_PATH payload; + EFI_DEVICE_PATH end; + } _packed_ payload_device_path = { + .payload = { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_VENDOR_DP, + .Length = sizeof(payload_device_path.payload), + }, + .Guid = STUB_PAYLOAD_GUID, + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = sizeof(payload_device_path.end), + }, + }; + + /* We want to support unsigned kernel images as payload, which is safe to do under secure boot + * because it is embedded in this stub loader (and since it is already running it must be trusted). */ + install_security_override( + validate_payload, + &(ValidationContext) { + .addr = source, + .len = len, + .device_path = &payload_device_path.payload.Header, + }); + + EFI_STATUS ret = BS->LoadImage( + /*BootPolicy=*/false, + parent, + &payload_device_path.payload.Header, + (void *) source, + len, + ret_image); + + uninstall_security_override(); + + return ret; +} + +EFI_STATUS linux_exec( + EFI_HANDLE parent, + const char16_t *cmdline, + const void *linux_buffer, + size_t linux_length, + const void *initrd_buffer, + size_t initrd_length) { + + uint32_t compat_address; + EFI_STATUS err; + + assert(parent); + assert(linux_buffer && linux_length > 0); + assert(initrd_buffer || initrd_length == 0); + + err = pe_kernel_info(linux_buffer, &compat_address); +#if defined(__i386__) || defined(__x86_64__) + if (err == EFI_UNSUPPORTED) + /* Kernel is too old to support LINUX_INITRD_MEDIA_GUID, try the deprecated EFI handover + * protocol. */ + return linux_exec_efi_handover( + parent, + cmdline, + linux_buffer, + linux_length, + initrd_buffer, + initrd_length); +#endif + if (err != EFI_SUCCESS) + return log_error_status(err, "Bad kernel image: %m"); + + _cleanup_(unload_imagep) EFI_HANDLE kernel_image = NULL; + err = load_image(parent, linux_buffer, linux_length, &kernel_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error loading kernel image: %m"); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol( + kernel_image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting kernel loaded image protocol: %m"); + + if (cmdline) { + loaded_image->LoadOptions = (void *) cmdline; + loaded_image->LoadOptionsSize = strsize16(loaded_image->LoadOptions); + } + + _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; + err = initrd_register(initrd_buffer, initrd_length, &initrd_handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error registering initrd: %m"); + + log_wait(); + err = BS->StartImage(kernel_image, NULL, NULL); + + /* Try calling the kernel compat entry point if one exists. */ + if (err == EFI_UNSUPPORTED && compat_address > 0) { + EFI_IMAGE_ENTRY_POINT compat_entry = + (EFI_IMAGE_ENTRY_POINT) ((uint8_t *) loaded_image->ImageBase + compat_address); + err = compat_entry(kernel_image, ST); + } + + return log_error_status(err, "Error starting kernel image: %m"); +} diff --git a/src/boot/efi/linux.h b/src/boot/efi/linux.h new file mode 100644 index 0000000..46b5f4f --- /dev/null +++ b/src/boot/efi/linux.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS linux_exec( + EFI_HANDLE parent, + const char16_t *cmdline, + const void *linux_buffer, + size_t linux_length, + const void *initrd_buffer, + size_t initrd_length); +EFI_STATUS linux_exec_efi_handover( + EFI_HANDLE parent, + const char16_t *cmdline, + const void *linux_buffer, + size_t linux_length, + const void *initrd_buffer, + size_t initrd_length); diff --git a/src/boot/efi/linux_x86.c b/src/boot/efi/linux_x86.c new file mode 100644 index 0000000..757902d --- /dev/null +++ b/src/boot/efi/linux_x86.c @@ -0,0 +1,224 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * x86 specific code to for EFI handover boot protocol + * Linux kernels version 5.8 and newer support providing the initrd by + * LINUX_INITRD_MEDIA_GUID DevicePath. In order to support older kernels too, + * this x86 specific linux_exec function passes the initrd by setting the + * corresponding fields in the setup_header struct. + * + * see https://docs.kernel.org/x86/boot.html + */ + +#include "initrd.h" +#include "linux.h" +#include "macro-fundamental.h" +#include "util.h" + +#define KERNEL_SECTOR_SIZE 512u +#define BOOT_FLAG_MAGIC 0xAA55u +#define SETUP_MAGIC 0x53726448u /* "HdrS" */ +#define SETUP_VERSION_2_11 0x20bu +#define SETUP_VERSION_2_12 0x20cu +#define SETUP_VERSION_2_15 0x20fu +#define CMDLINE_PTR_MAX 0xA0000u + +enum { + XLF_KERNEL_64 = 1 << 0, + XLF_CAN_BE_LOADED_ABOVE_4G = 1 << 1, + XLF_EFI_HANDOVER_32 = 1 << 2, + XLF_EFI_HANDOVER_64 = 1 << 3, +#ifdef __x86_64__ + XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_64, +#else + XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_32, +#endif +}; + +typedef struct { + uint8_t setup_sects; + uint16_t root_flags; + uint32_t syssize; + uint16_t ram_size; + uint16_t vid_mode; + uint16_t root_dev; + uint16_t boot_flag; + uint8_t jump; /* We split the 2-byte jump field from the spec in two for convenience. */ + uint8_t setup_size; + uint32_t header; + uint16_t version; + uint32_t realmode_swtch; + uint16_t start_sys_seg; + uint16_t kernel_version; + uint8_t type_of_loader; + uint8_t loadflags; + uint16_t setup_move_size; + uint32_t code32_start; + uint32_t ramdisk_image; + uint32_t ramdisk_size; + uint32_t bootsect_kludge; + uint16_t heap_end_ptr; + uint8_t ext_loader_ver; + uint8_t ext_loader_type; + uint32_t cmd_line_ptr; + uint32_t initrd_addr_max; + uint32_t kernel_alignment; + uint8_t relocatable_kernel; + uint8_t min_alignment; + uint16_t xloadflags; + uint32_t cmdline_size; + uint32_t hardware_subarch; + uint64_t hardware_subarch_data; + uint32_t payload_offset; + uint32_t payload_length; + uint64_t setup_data; + uint64_t pref_address; + uint32_t init_size; + uint32_t handover_offset; +} _packed_ SetupHeader; + +/* We really only care about a few fields, but we still have to provide a full page otherwise. */ +typedef struct { + uint8_t pad[192]; + uint32_t ext_ramdisk_image; + uint32_t ext_ramdisk_size; + uint32_t ext_cmd_line_ptr; + uint8_t pad2[293]; + SetupHeader hdr; + uint8_t pad3[3480]; +} _packed_ BootParams; +assert_cc(offsetof(BootParams, ext_ramdisk_image) == 0x0C0); +assert_cc(sizeof(BootParams) == 4096); + +#ifdef __i386__ +# define __regparm0__ __attribute__((regparm(0))) +#else +# define __regparm0__ +#endif + +typedef void (*handover_f)(void *parent, EFI_SYSTEM_TABLE *table, BootParams *params) __regparm0__ + __attribute__((sysv_abi)); + +static void linux_efi_handover(EFI_HANDLE parent, uintptr_t kernel, BootParams *params) { + assert(params); + + kernel += (params->hdr.setup_sects + 1) * KERNEL_SECTOR_SIZE; /* 32-bit entry address. */ + + /* Old kernels needs this set, while newer ones seem to ignore this. */ + params->hdr.code32_start = kernel; + +#ifdef __x86_64__ + kernel += KERNEL_SECTOR_SIZE; /* 64-bit entry address. */ +#endif + + kernel += params->hdr.handover_offset; /* 32/64-bit EFI handover address. */ + + /* Note in EFI mixed mode this now points to the correct 32-bit handover entry point, allowing a 64-bit + * kernel to be booted from a 32-bit sd-stub. */ + + handover_f handover = (handover_f) kernel; + handover(parent, ST, params); +} + +EFI_STATUS linux_exec_efi_handover( + EFI_HANDLE parent, + const char16_t *cmdline, + const void *linux_buffer, + size_t linux_length, + const void *initrd_buffer, + size_t initrd_length) { + + assert(parent); + assert(linux_buffer); + assert(initrd_buffer || initrd_length == 0); + + if (linux_length < sizeof(BootParams)) + return EFI_LOAD_ERROR; + + const BootParams *image_params = (const BootParams *) linux_buffer; + if (image_params->hdr.header != SETUP_MAGIC || image_params->hdr.boot_flag != BOOT_FLAG_MAGIC) + return log_error_status(EFI_UNSUPPORTED, "Unsupported kernel image."); + if (image_params->hdr.version < SETUP_VERSION_2_11) + return log_error_status(EFI_UNSUPPORTED, "Kernel too old."); + if (!image_params->hdr.relocatable_kernel) + return log_error_status(EFI_UNSUPPORTED, "Kernel is not relocatable."); + + /* The xloadflags were added in version 2.12+ of the boot protocol but the handover support predates + * that, so we cannot safety-check this for 2.11. */ + if (image_params->hdr.version >= SETUP_VERSION_2_12 && + !FLAGS_SET(image_params->hdr.xloadflags, XLF_EFI_HANDOVER)) + return log_error_status(EFI_UNSUPPORTED, "Kernel does not support EFI handover protocol."); + + bool can_4g = image_params->hdr.version >= SETUP_VERSION_2_12 && + FLAGS_SET(image_params->hdr.xloadflags, XLF_CAN_BE_LOADED_ABOVE_4G); + + /* There is no way to pass the high bits of code32_start. Newer kernels seems to handle this + * just fine, but older kernels will fail even if they otherwise have above 4G boot support. */ + _cleanup_pages_ Pages linux_relocated = {}; + if (POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + linux_length > UINT32_MAX) { + linux_relocated = xmalloc_pages( + AllocateMaxAddress, EfiLoaderCode, EFI_SIZE_TO_PAGES(linux_length), UINT32_MAX); + linux_buffer = memcpy( + PHYSICAL_ADDRESS_TO_POINTER(linux_relocated.addr), linux_buffer, linux_length); + } + + _cleanup_pages_ Pages initrd_relocated = {}; + if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) + initrd_length > UINT32_MAX) { + initrd_relocated = xmalloc_pages( + AllocateMaxAddress, EfiLoaderData, EFI_SIZE_TO_PAGES(initrd_length), UINT32_MAX); + initrd_buffer = memcpy( + PHYSICAL_ADDRESS_TO_POINTER(initrd_relocated.addr), + initrd_buffer, + initrd_length); + } + + _cleanup_pages_ Pages boot_params_page = xmalloc_pages( + can_4g ? AllocateAnyPages : AllocateMaxAddress, + EfiLoaderData, + EFI_SIZE_TO_PAGES(sizeof(BootParams)), + UINT32_MAX /* Below the 4G boundary */); + BootParams *boot_params = PHYSICAL_ADDRESS_TO_POINTER(boot_params_page.addr); + *boot_params = (BootParams){}; + + /* Setup size is determined by offset 0x0202 + byte value at offset 0x0201, which is the same as + * offset of the header field and the target from the jump field (which we split for this reason). */ + memcpy(&boot_params->hdr, + &image_params->hdr, + offsetof(SetupHeader, header) + image_params->hdr.setup_size); + + boot_params->hdr.type_of_loader = 0xff; + + /* Spec says: For backwards compatibility, if the setup_sects field contains 0, the real value is 4. */ + if (boot_params->hdr.setup_sects == 0) + boot_params->hdr.setup_sects = 4; + + _cleanup_pages_ Pages cmdline_pages = {}; + if (cmdline) { + size_t len = MIN(strlen16(cmdline), image_params->hdr.cmdline_size); + + cmdline_pages = xmalloc_pages( + can_4g ? AllocateAnyPages : AllocateMaxAddress, + EfiLoaderData, + EFI_SIZE_TO_PAGES(len + 1), + CMDLINE_PTR_MAX); + + /* Convert cmdline to ASCII. */ + char *cmdline8 = PHYSICAL_ADDRESS_TO_POINTER(cmdline_pages.addr); + for (size_t i = 0; i < len; i++) + cmdline8[i] = cmdline[i] <= 0x7E ? cmdline[i] : ' '; + cmdline8[len] = '\0'; + + boot_params->hdr.cmd_line_ptr = (uint32_t) cmdline_pages.addr; + boot_params->ext_cmd_line_ptr = cmdline_pages.addr >> 32; + assert(can_4g || cmdline_pages.addr <= CMDLINE_PTR_MAX); + } + + boot_params->hdr.ramdisk_image = (uintptr_t) initrd_buffer; + boot_params->ext_ramdisk_image = POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) >> 32; + boot_params->hdr.ramdisk_size = initrd_length; + boot_params->ext_ramdisk_size = ((uint64_t) initrd_length) >> 32; + + log_wait(); + linux_efi_handover(parent, (uintptr_t) linux_buffer, boot_params); + return EFI_LOAD_ERROR; +} diff --git a/src/boot/efi/log.c b/src/boot/efi/log.c new file mode 100644 index 0000000..364471e --- /dev/null +++ b/src/boot/efi/log.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "log.h" +#include "proto/rng.h" +#include "proto/simple-text-io.h" +#include "util.h" + +static unsigned log_count = 0; + +void freeze(void) { + for (;;) + BS->Stall(60 * 1000 * 1000); +} + +_noreturn_ static void panic(const char16_t *message) { + if (ST->ConOut->Mode->CursorColumn > 0) + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, EFI_TEXT_ATTR(EFI_LIGHTRED, EFI_BLACK)); + ST->ConOut->OutputString(ST->ConOut, (char16_t *) message); + freeze(); +} + +void efi_assert(const char *expr, const char *file, unsigned line, const char *function) { + static bool asserting = false; + + /* Let's be paranoid. */ + if (asserting) + panic(u"systemd-boot: Nested assertion failure, halting."); + + asserting = true; + log_error("systemd-boot: Assertion '%s' failed at %s:%u@%s, halting.", expr, file, line, function); + freeze(); +} + +EFI_STATUS log_internal(EFI_STATUS status, const char *format, ...) { + assert(format); + + int32_t attr = ST->ConOut->Mode->Attribute; + + if (ST->ConOut->Mode->CursorColumn > 0) + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, EFI_TEXT_ATTR(EFI_LIGHTRED, EFI_BLACK)); + + va_list ap; + va_start(ap, format); + vprintf_status(status, format, ap); + va_end(ap); + + ST->ConOut->OutputString(ST->ConOut, (char16_t *) u"\r\n"); + ST->ConOut->SetAttribute(ST->ConOut, attr); + + log_count++; + return status; +} + +#ifdef EFI_DEBUG +void log_hexdump(const char16_t *prefix, const void *data, size_t size) { + /* Debugging helper — please keep this around, even if not used */ + + _cleanup_free_ char16_t *hex = hexdump(data, size); + log_internal(EFI_SUCCESS, "%ls[%zu]: %ls", prefix, size, hex); +} +#endif + +void log_wait(void) { + if (log_count == 0) + return; + + BS->Stall(MIN(4u, log_count) * 2500 * 1000); + log_count = 0; +} + +_used_ intptr_t __stack_chk_guard = (intptr_t) 0x70f6967de78acae3; + +/* We can only set a random stack canary if this function attribute is available, + * otherwise this may create a stack check fail. */ +#if STACK_PROTECTOR_RANDOM +void __stack_chk_guard_init(void) { + EFI_RNG_PROTOCOL *rng; + if (BS->LocateProtocol(MAKE_GUID_PTR(EFI_RNG_PROTOCOL), NULL, (void **) &rng) == EFI_SUCCESS) + (void) rng->GetRNG(rng, NULL, sizeof(__stack_chk_guard), (void *) &__stack_chk_guard); + else + /* Better than no extra entropy. */ + __stack_chk_guard ^= (intptr_t) __executable_start; +} +#endif + +_used_ _noreturn_ void __stack_chk_fail(void); +_used_ _noreturn_ void __stack_chk_fail_local(void); +void __stack_chk_fail(void) { + panic(u"systemd-boot: Stack check failed, halting."); +} +void __stack_chk_fail_local(void) { + __stack_chk_fail(); +} + +/* Called by libgcc for some fatal errors like integer overflow with -ftrapv. */ +_used_ _noreturn_ void abort(void); +void abort(void) { + panic(u"systemd-boot: Unknown error, halting."); +} + +#if defined(__ARM_EABI__) +/* These override the (weak) div0 handlers from libgcc as they would otherwise call raise() instead. */ +_used_ _noreturn_ int __aeabi_idiv0(int return_value); +_used_ _noreturn_ long long __aeabi_ldiv0(long long return_value); + +int __aeabi_idiv0(int return_value) { + panic(u"systemd-boot: Division by zero, halting."); +} + +long long __aeabi_ldiv0(long long return_value) { + panic(u"systemd-boot: Division by zero, halting."); +} +#endif diff --git a/src/boot/efi/log.h b/src/boot/efi/log.h new file mode 100644 index 0000000..13f3887 --- /dev/null +++ b/src/boot/efi/log.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi-string.h" + +#if defined __has_attribute +# if __has_attribute(no_stack_protector) +# define HAVE_NO_STACK_PROTECTOR_ATTRIBUTE +# endif +#endif + +#if defined(HAVE_NO_STACK_PROTECTOR_ATTRIBUTE) && \ + (defined(__SSP__) || defined(__SSP_ALL__) || \ + defined(__SSP_STRONG__) || defined(__SSP_EXPLICIT__)) +# define STACK_PROTECTOR_RANDOM 1 +__attribute__((no_stack_protector, noinline)) void __stack_chk_guard_init(void); +#else +# define STACK_PROTECTOR_RANDOM 0 +# define __stack_chk_guard_init() +#endif + +_noreturn_ void freeze(void); +void log_wait(void); +_gnu_printf_(2, 3) EFI_STATUS log_internal(EFI_STATUS status, const char *format, ...); +#define log_error_status(status, ...) log_internal(status, __VA_ARGS__) +#define log_error(...) log_internal(EFI_INVALID_PARAMETER, __VA_ARGS__) +#define log_oom() log_internal(EFI_OUT_OF_RESOURCES, "Out of memory.") +#define log_trace() log_internal(EFI_SUCCESS, "%s:%i@%s", __FILE__, __LINE__, __func__) + +#ifdef EFI_DEBUG +void log_hexdump(const char16_t *prefix, const void *data, size_t size); +#endif diff --git a/src/boot/efi/measure.c b/src/boot/efi/measure.c new file mode 100644 index 0000000..01c97c8 --- /dev/null +++ b/src/boot/efi/measure.c @@ -0,0 +1,296 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if ENABLE_TPM + +#include "macro-fundamental.h" +#include "measure.h" +#include "memory-util-fundamental.h" +#include "proto/tcg.h" +#include "tpm2-pcr.h" +#include "util.h" + +static EFI_STATUS tpm1_measure_to_pcr_and_event_log( + const EFI_TCG_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + size_t buffer_size, + const char16_t *description) { + + _cleanup_free_ TCG_PCR_EVENT *tcg_event = NULL; + EFI_PHYSICAL_ADDRESS event_log_last; + uint32_t event_number = 1; + size_t desc_len; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + tcg_event = xmalloc(offsetof(TCG_PCR_EVENT, Event) + desc_len); + *tcg_event = (TCG_PCR_EVENT) { + .EventSize = desc_len, + .PCRIndex = pcrindex, + .EventType = EV_IPL, + }; + memcpy(tcg_event->Event, description, desc_len); + + return tcg->HashLogExtendEvent( + (EFI_TCG_PROTOCOL *) tcg, + buffer, buffer_size, + TCG_ALG_SHA, + tcg_event, + &event_number, + &event_log_last); +} + +static EFI_STATUS tpm2_measure_to_pcr_and_tagged_event_log( + EFI_TCG2_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + uint64_t buffer_size, + uint32_t event_id, + const char16_t *description) { + + _cleanup_free_ struct event { + EFI_TCG2_EVENT tcg_event; + EFI_TCG2_TAGGED_EVENT tcg_tagged_event; + } _packed_ *event = NULL; + size_t desc_len, event_size; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + event_size = offsetof(EFI_TCG2_EVENT, Event) + offsetof(EFI_TCG2_TAGGED_EVENT, Event) + desc_len; + + event = xmalloc(event_size); + *event = (struct event) { + .tcg_event = (EFI_TCG2_EVENT) { + .Size = event_size, + .Header.HeaderSize = sizeof(EFI_TCG2_EVENT_HEADER), + .Header.HeaderVersion = EFI_TCG2_EVENT_HEADER_VERSION, + .Header.PCRIndex = pcrindex, + .Header.EventType = EV_EVENT_TAG, + }, + .tcg_tagged_event = { + .EventId = event_id, + .EventSize = desc_len, + }, + }; + memcpy(event->tcg_tagged_event.Event, description, desc_len); + + return tcg->HashLogExtendEvent( + tcg, + 0, + buffer, buffer_size, + &event->tcg_event); +} + +static EFI_STATUS tpm2_measure_to_pcr_and_event_log( + EFI_TCG2_PROTOCOL *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + uint64_t buffer_size, + const char16_t *description) { + + _cleanup_free_ EFI_TCG2_EVENT *tcg_event = NULL; + size_t desc_len; + + assert(tcg); + assert(description); + + /* NB: We currently record everything as EV_IPL. Which sucks, because it makes it hard to + * recognize from the event log which of the events are ours. Measurement logs are kinda API hence + * this is hard to change for existing, established events. But for future additions, let's use + * EV_EVENT_TAG instead, with a tag of our choosing that makes clear what precisely we are measuring + * here. */ + + desc_len = strsize16(description); + tcg_event = xmalloc(offsetof(EFI_TCG2_EVENT, Event) + desc_len); + *tcg_event = (EFI_TCG2_EVENT) { + .Size = offsetof(EFI_TCG2_EVENT, Event) + desc_len, + .Header.HeaderSize = sizeof(EFI_TCG2_EVENT_HEADER), + .Header.HeaderVersion = EFI_TCG2_EVENT_HEADER_VERSION, + .Header.PCRIndex = pcrindex, + .Header.EventType = EV_IPL, + }; + + memcpy(tcg_event->Event, description, desc_len); + + return tcg->HashLogExtendEvent( + tcg, + 0, + buffer, buffer_size, + tcg_event); +} + +static EFI_TCG_PROTOCOL *tcg1_interface_check(void) { + EFI_PHYSICAL_ADDRESS event_log_location, event_log_last_entry; + EFI_TCG_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + uint32_t features; + EFI_TCG_PROTOCOL *tcg; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_TCG_PROTOCOL), NULL, (void **) &tcg); + if (err != EFI_SUCCESS) + return NULL; + + err = tcg->StatusCheck( + tcg, + &capability, + &features, + &event_log_location, + &event_log_last_entry); + if (err != EFI_SUCCESS) + return NULL; + + if (capability.TPMDeactivatedFlag) + return NULL; + + if (!capability.TPMPresentFlag) + return NULL; + + return tcg; +} + +static EFI_TCG2_PROTOCOL *tcg2_interface_check(void) { + EFI_TCG2_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + EFI_TCG2_PROTOCOL *tcg; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_TCG2_PROTOCOL), NULL, (void **) &tcg); + if (err != EFI_SUCCESS) + return NULL; + + err = tcg->GetCapability(tcg, &capability); + if (err != EFI_SUCCESS) + return NULL; + + if (capability.StructureVersion.Major == 1 && + capability.StructureVersion.Minor == 0) { + EFI_TCG_BOOT_SERVICE_CAPABILITY *caps_1_0 = + (EFI_TCG_BOOT_SERVICE_CAPABILITY*) &capability; + if (caps_1_0->TPMPresentFlag) + return tcg; + } + + if (!capability.TPMPresentFlag) + return NULL; + + return tcg; +} + +bool tpm_present(void) { + return tcg2_interface_check() || tcg1_interface_check(); +} + +EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char16_t *description, bool *ret_measured) { + EFI_TCG2_PROTOCOL *tpm2; + EFI_STATUS err; + + assert(description || pcrindex == UINT32_MAX); + + /* If EFI_SUCCESS is returned, will initialize ret_measured to true if we actually measured + * something, or false if measurement was turned off. */ + + if (pcrindex == UINT32_MAX) { /* PCR disabled? */ + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; + } + + tpm2 = tcg2_interface_check(); + if (tpm2) + err = tpm2_measure_to_pcr_and_event_log(tpm2, pcrindex, buffer, buffer_size, description); + else { + EFI_TCG_PROTOCOL *tpm1; + + tpm1 = tcg1_interface_check(); + if (tpm1) + err = tpm1_measure_to_pcr_and_event_log(tpm1, pcrindex, buffer, buffer_size, description); + else { + /* No active TPM found, so don't return an error */ + + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; + } + } + + if (err == EFI_SUCCESS && ret_measured) + *ret_measured = true; + + return err; +} + +EFI_STATUS tpm_log_tagged_event( + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + size_t buffer_size, + uint32_t event_id, + const char16_t *description, + bool *ret_measured) { + + EFI_TCG2_PROTOCOL *tpm2; + EFI_STATUS err; + + assert(description || pcrindex == UINT32_MAX); + assert(event_id > 0); + + /* If EFI_SUCCESS is returned, will initialize ret_measured to true if we actually measured + * something, or false if measurement was turned off. */ + + tpm2 = tcg2_interface_check(); + if (!tpm2 || pcrindex == UINT32_MAX) { /* PCR disabled? */ + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; + } + + err = tpm2_measure_to_pcr_and_tagged_event_log(tpm2, pcrindex, buffer, buffer_size, event_id, description); + if (err == EFI_SUCCESS && ret_measured) + *ret_measured = true; + + return err; +} + +EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char *description, bool *ret_measured) { + _cleanup_free_ char16_t *c = NULL; + + if (description) + c = xstr8_to_16(description); + + return tpm_log_event(pcrindex, buffer, buffer_size, c, ret_measured); +} + +EFI_STATUS tpm_log_load_options(const char16_t *load_options, bool *ret_measured) { + bool measured = false; + EFI_STATUS err; + + /* Measures a load options string into the TPM2, i.e. the kernel command line */ + + err = tpm_log_event( + TPM2_PCR_KERNEL_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(load_options), + strsize16(load_options), + load_options, + &measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add load options (i.e. kernel command) line measurement to PCR %i: %m", + TPM2_PCR_KERNEL_CONFIG); + + if (ret_measured) + *ret_measured = measured; + + return EFI_SUCCESS; +} + +#endif diff --git a/src/boot/efi/measure.h b/src/boot/efi/measure.h new file mode 100644 index 0000000..c3c4e0a --- /dev/null +++ b/src/boot/efi/measure.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#if ENABLE_TPM + +bool tpm_present(void); +EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char16_t *description, bool *ret_measured); +EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char *description, bool *ret_measured); +EFI_STATUS tpm_log_tagged_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, uint32_t event_id, const char16_t *description, bool *ret_measured); +EFI_STATUS tpm_log_load_options(const char16_t *cmdline, bool *ret_measured); + +#else + +static inline bool tpm_present(void) { + return false; +} + +static inline EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char16_t *description, bool *ret_measured) { + if (ret_measured) + *ret_measured = false; + return EFI_SUCCESS; +} + +static inline EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, const char *description, bool *ret_measured) { + if (ret_measured) + *ret_measured = false; + return EFI_SUCCESS; +} + +static inline EFI_STATUS tpm_log_tagged_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, size_t buffer_size, uint32_t event_id, const char16_t *description, bool *ret_measured) { + if (ret_measured) + *ret_measured = false; + return EFI_SUCCESS; +} + +static inline EFI_STATUS tpm_log_load_options(const char16_t *cmdline, bool *ret_measured) { + if (ret_measured) + *ret_measured = false; + return EFI_SUCCESS; +} + +#endif diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build new file mode 100644 index 0000000..c95132e --- /dev/null +++ b/src/boot/efi/meson.build @@ -0,0 +1,409 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +efi_config_h_dir = meson.current_build_dir() +efi_addon = '' + +libefitest = static_library( + 'efitest', + files( + 'bcd.c', + 'efi-string.c', + ), + build_by_default : false, + include_directories : [ + basic_includes, + include_directories('.'), + ], + dependencies : userspace) + +efitest_base = { + 'link_with' : [ + libefitest, + libshared, + ], +} +efi_test_template = test_template + efitest_base +efi_fuzz_template = fuzz_template + efitest_base + +executables += [ + efi_test_template + { + 'sources' : files('test-bcd.c'), + 'dependencies' : libzstd, + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_ZSTD'], + }, + efi_test_template + { + 'sources' : files('test-efi-string.c'), + 'conditions' : ['ENABLE_BOOTLOADER'], + }, + efi_fuzz_template + { + 'sources' : files('fuzz-bcd.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-string.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-osrel.c'), + }, + efi_fuzz_template + { + 'sources' : files('fuzz-efi-printf.c'), + }, +] + +if conf.get('ENABLE_BOOTLOADER') != 1 + subdir_done() +endif + +efi_conf = configuration_data() +efi_conf.set10('ENABLE_TPM', get_option('tpm')) + +foreach ctype : ['color-normal', 'color-entry', 'color-highlight', 'color-edit'] + c = get_option('efi-' + ctype).split(',') + efi_conf.set(ctype.underscorify().to_upper(), 'EFI_TEXT_ATTR(@0@, @1@)'.format( + 'EFI_' + c[0].strip().underscorify().to_upper(), + 'EFI_' + c[1].strip().underscorify().to_upper())) +endforeach + +if meson.is_cross_build() and get_option('sbat-distro') == 'auto' + warning('Auto detection of SBAT information not supported when cross-building, disabling SBAT.') +elif get_option('sbat-distro') != '' + efi_conf.set_quoted('SBAT_PROJECT', meson.project_name()) + efi_conf.set_quoted('PROJECT_VERSION', meson.project_version()) + efi_conf.set('PROJECT_URL', conf.get('PROJECT_URL')) + if get_option('sbat-distro-generation') < 1 + error('SBAT Distro Generation must be a positive integer') + endif + efi_conf.set('SBAT_DISTRO_GENERATION', get_option('sbat-distro-generation')) + foreach sbatvar : [['sbat-distro', 'ID'], + ['sbat-distro-summary', 'NAME'], + ['sbat-distro-url', 'BUG_REPORT_URL']] + value = get_option(sbatvar[0]) + if (value == '' or value == 'auto') and not meson.is_cross_build() + cmd = 'if [ -e /etc/os-release ]; then . /etc/os-release; else . /usr/lib/os-release; fi; echo $@0@'.format(sbatvar[1]) + value = run_command(sh, '-c', cmd, check: true).stdout().strip() + endif + if value == '' + error('Required @0@ option not set and autodetection failed'.format(sbatvar[0])) + endif + efi_conf.set_quoted(sbatvar[0].underscorify().to_upper(), value) + endforeach + + pkgname = get_option('sbat-distro-pkgname') + if pkgname == '' + pkgname = meson.project_name() + endif + efi_conf.set_quoted('SBAT_DISTRO_PKGNAME', pkgname) + + pkgver = get_option('sbat-distro-version') + if pkgver == '' + # This is determined during build, not configuration, so we can't display it yet. + efi_conf.set('SBAT_DISTRO_VERSION', 'GIT_VERSION') + else + efi_conf.set_quoted('SBAT_DISTRO_VERSION', pkgver) + endif +endif + +summary({'UEFI architectures' : efi_arch + (efi_arch_alt == '' ? '' : ', ' + efi_arch_alt)}, + section : 'UEFI') + +if efi_conf.get('SBAT_DISTRO', '') != '' + summary({ + 'SBAT distro': efi_conf.get('SBAT_DISTRO'), + 'SBAT distro generation': efi_conf.get('SBAT_DISTRO_GENERATION'), + 'SBAT distro version': efi_conf.get('SBAT_DISTRO_VERSION'), + 'SBAT distro summary': efi_conf.get('SBAT_DISTRO_SUMMARY'), + 'SBAT distro URL': efi_conf.get('SBAT_DISTRO_URL')}, + section : 'UEFI') +endif + +configure_file( + output : 'efi_config.h', + configuration : efi_conf) + +############################################################ + +efi_includes = [ + build_dir_include, + fundamental_include, + include_directories('.'), +] + +efi_c_args = [ + '-DSD_BOOT=1', + '-ffreestanding', + '-fno-strict-aliasing', + '-fshort-wchar', + '-include', 'efi_config.h', +] + +efi_c_args += cc.get_supported_arguments( + '-fwide-exec-charset=UCS2', + # gcc docs says this is required for ms_abi to work correctly. + '-maccumulate-outgoing-args', + '-mstack-protector-guard=global', +) + +# Debug information has little value in release builds as no normal human being knows +# how to attach a debugger to EFI binaries running on real hardware. Anyone who does +# certainly has the means to do their own dev build. +if get_option('mode') == 'developer' and get_option('debug') + efi_c_args += '-DEFI_DEBUG' +endif + +efi_c_ld_args = [ + '-lgcc', + '-nostdlib', + '-static-pie', + '-Wl,--entry=efi_main', + '-Wl,--fatal-warnings', + + # These flags should be passed by -static-pie, but for whatever reason the flag translation + # is not enabled on all architectures. Not passing `-static` would just allow the linker to + # use dynamic libraries, (which we can't/don't use anyway). But if `-pie` is missing and the + # gcc build does not default to `-pie` we get a regular (no-pie) binary that will be + # rightfully rejected by elf2efi. Note that meson also passes `-pie` to the linker driver, + # but it is overridden by our `-static-pie`. We also need to pass these directly to the + # linker as `-static`+`-pie` seem to get translated differently. + '-Wl,-static,-pie,--no-dynamic-linker,-z,text', + + # EFI has 4KiB pages. + '-z', 'common-page-size=4096', + '-z', 'max-page-size=4096', + + '-z', 'noexecstack', + '-z', 'relro', + '-z', 'separate-code', +] + +efi_c_ld_args += cc.get_supported_link_arguments( + # binutils >= 2.38 + '-Wl,-z,nopack-relative-relocs', +) + +# efi_c_args is explicitly passed to targets so that they can override distro-provided flags +# that should not be used for EFI binaries. +efi_disabled_c_args = cc.get_supported_arguments( + '-fcf-protection=none', + '-fno-asynchronous-unwind-tables', + '-fno-exceptions', + '-fno-unwind-tables', +) +efi_override_options = [ + 'b_coverage=false', + 'b_pgo=off', +] + +if get_option('b_sanitize') == 'undefined' + efi_disabled_c_args += cc.get_supported_arguments('-fno-sanitize-link-runtime') +else + efi_disabled_c_args += cc.get_supported_arguments('-fno-sanitize=all') + efi_override_options += 'b_sanitize=none' +endif + +efi_c_args += efi_disabled_c_args +efi_c_ld_args += efi_disabled_c_args + +if cc.get_id() == 'clang' + # clang is too picky sometimes. + efi_c_args += '-Wno-unused-command-line-argument' + efi_c_ld_args += '-Wno-unused-command-line-argument' +endif + +efi_arch_c_args = { + 'aarch64' : ['-mgeneral-regs-only'], + 'arm' : ['-mgeneral-regs-only'], + # Pass -m64/32 explicitly to make building on x32 work. + 'x86_64' : ['-m64', '-march=x86-64', '-mno-red-zone', '-mgeneral-regs-only'], + 'x86' : ['-m32', '-march=i686', '-mgeneral-regs-only', '-malign-double'], +} +efi_arch_c_ld_args = { + # libgcc is not compiled with -fshort-wchar, but it does not use it anyways, + # so it's fine to link against it. + 'arm' : cc.get_supported_link_arguments('-Wl,--no-wchar-size-warning'), + 'x86_64' : ['-m64'], + 'x86' : ['-m32'], +} + +linker_sanity_code = 'void a(void) {}; void _start(void) { a(); }' +linker_sanity_args = ['-nostdlib', '-Wl,--fatal-warnings'] +if not cc.links(linker_sanity_code, + name : 'linker supports -static-pie', + args : [linker_sanity_args, '-static-pie']) + error('Linker does not support -static-pie.') +endif + +# https://github.com/llvm/llvm-project/issues/67152 +if not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib', + args : [linker_sanity_args, '-flto']) + efi_c_args += '-fno-lto' + efi_c_ld_args += '-fno-lto' +endif + +# https://github.com/llvm/llvm-project/issues/61101 +if efi_cpu_family_alt == 'x86' and not cc.links(linker_sanity_code, + name : 'linker supports LTO with -nostdlib (x86)', + args : [linker_sanity_args, '-flto', '-m32']) + efi_arch_c_args += { 'x86' : efi_arch_c_args['x86'] + '-fno-lto' } + efi_arch_c_ld_args += { 'x86' : efi_arch_c_ld_args['x86'] + '-fno-lto' } +endif + +############################################################ + +libefi_sources = files( + 'console.c', + 'device-path-util.c', + 'devicetree.c', + 'drivers.c', + 'efi-string.c', + 'graphics.c', + 'initrd.c', + 'log.c', + 'measure.c', + 'part-discovery.c', + 'pe.c', + 'random-seed.c', + 'secure-boot.c', + 'shim.c', + 'ticks.c', + 'util.c', + 'vmm.c', +) + +systemd_boot_sources = files( + 'boot.c', +) + +stub_sources = files( + 'cpio.c', + 'linux.c', + 'splash.c', + 'stub.c', +) + +addon_sources = files( + 'addon.c', +) + +if get_option('b_sanitize') == 'undefined' + libefi_sources += files('ubsan.c') +endif + +if host_machine.cpu_family() in ['x86', 'x86_64'] + stub_sources += files('linux_x86.c') +endif + +# BCD parser only makes sense on arches that Windows supports. +if host_machine.cpu_family() in ['aarch64', 'arm', 'x86_64', 'x86'] + systemd_boot_sources += files('bcd.c') +endif + +boot_targets = [] +efi_elf_binaries = [] +efi_archspecs = [ + { + 'arch' : efi_arch, + 'c_args' : [ + efi_c_args, + '-DEFI_MACHINE_TYPE_NAME="' + efi_arch + '"', + efi_arch_c_args.get(host_machine.cpu_family(), []), + ], + 'link_args' : [ + efi_c_ld_args, + efi_arch_c_ld_args.get(host_machine.cpu_family(), []), + ], + }, +] +if efi_arch_alt != '' + efi_archspecs += { + 'arch' : efi_arch_alt, + 'c_args' : [ + efi_c_args, + '-DEFI_MACHINE_TYPE_NAME="' + efi_arch_alt + '"', + efi_arch_c_args.get(efi_cpu_family_alt, []), + ], + 'link_args' : [ + efi_c_ld_args, + efi_arch_c_ld_args.get(efi_cpu_family_alt, []), + ], + } +endif + +foreach archspec : efi_archspecs + libefi = static_library( + 'efi' + archspec['arch'], + fundamental_sources, + libefi_sources, + version_h, + include_directories : efi_includes, + c_args : archspec['c_args'], + gnu_symbol_visibility : 'hidden', + override_options : efi_override_options, + pic : true) + + kwargs = { + 'include_directories' : efi_includes, + 'c_args' : archspec['c_args'], + 'link_args' : archspec['link_args'], + 'gnu_symbol_visibility' : 'hidden', + 'override_options' : efi_override_options, + 'pie' : true, + } + + efi_elf_binaries += executable( + 'systemd-boot' + archspec['arch'], + sources : [systemd_boot_sources, version_h], + link_with : libefi, + name_suffix : 'elf', + kwargs : kwargs) + + efi_elf_binaries += executable( + 'linux' + archspec['arch'], + sources : [stub_sources, version_h], + link_with : libefi, + name_suffix : 'elf.stub', + kwargs : kwargs) + + efi_elf_binaries += executable( + 'addon' + archspec['arch'], + sources : [addon_sources, version_h], + name_suffix : 'elf.stub', + kwargs : kwargs) +endforeach + +foreach efi_elf_binary : efi_elf_binaries + name = efi_elf_binary.name() + name += name.startswith('systemd-boot') ? '.efi' : '.efi.stub' + # For the addon, given it's empty, we need to explicitly reserve space in the header to account for + # the sections that ukify will add. + minimum_sections = name.endswith('.stub') ? '15' : '0' + exe = custom_target( + name, + output : name, + input : efi_elf_binary, + install : true, + install_dir : bootlibdir, + install_tag : 'systemd-boot', + command : [ + elf2efi_py, + '--version-major=' + meson.project_version(), + '--version-minor=0', + '--efi-major=1', + '--efi-minor=1', + '--subsystem=10', + '--minimum-sections=' + minimum_sections, + '--copy-sections=.sbat,.sdmagic,.osrel', + '@INPUT@', + '@OUTPUT@', + ]) + boot_targets += exe + if name.startswith('linux') + boot_stubs += exe + endif + + # This is supposed to match exactly one time + if name == 'addon@0@.efi.stub'.format(efi_arch) + efi_addon = exe.full_path() + endif +endforeach + +alias_target('systemd-boot', boot_targets) diff --git a/src/boot/efi/part-discovery.c b/src/boot/efi/part-discovery.c new file mode 100644 index 0000000..f5b1573 --- /dev/null +++ b/src/boot/efi/part-discovery.c @@ -0,0 +1,298 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "part-discovery.h" +#include "proto/block-io.h" +#include "proto/device-path.h" +#include "util.h" + +typedef struct { + EFI_GUID PartitionTypeGUID; + EFI_GUID UniquePartitionGUID; + EFI_LBA StartingLBA; + EFI_LBA EndingLBA; + uint64_t Attributes; + char16_t PartitionName[36]; +} EFI_PARTITION_ENTRY; + +typedef struct { + EFI_TABLE_HEADER Header; + EFI_LBA MyLBA; + EFI_LBA AlternateLBA; + EFI_LBA FirstUsableLBA; + EFI_LBA LastUsableLBA; + EFI_GUID DiskGUID; + EFI_LBA PartitionEntryLBA; + uint32_t NumberOfPartitionEntries; + uint32_t SizeOfPartitionEntry; + uint32_t PartitionEntryArrayCRC32; + uint8_t _pad[420]; +} _packed_ GptHeader; +assert_cc(sizeof(GptHeader) == 512); + +static bool verify_gpt(/*const*/ GptHeader *h, EFI_LBA lba_expected) { + uint32_t crc32, crc32_saved; + EFI_STATUS err; + + assert(h); + + /* Some superficial validation of the GPT header */ + if (memcmp(&h->Header.Signature, "EFI PART", sizeof(h->Header.Signature)) != 0) + return false; + + if (h->Header.HeaderSize < 92 || h->Header.HeaderSize > 512) + return false; + + if (h->Header.Revision != 0x00010000U) + return false; + + /* Calculate CRC check */ + crc32_saved = h->Header.CRC32; + h->Header.CRC32 = 0; + err = BS->CalculateCrc32(h, h->Header.HeaderSize, &crc32); + h->Header.CRC32 = crc32_saved; + if (err != EFI_SUCCESS || crc32 != crc32_saved) + return false; + + if (h->MyLBA != lba_expected) + return false; + + if ((h->SizeOfPartitionEntry % sizeof(EFI_PARTITION_ENTRY)) != 0) + return false; + + if (h->NumberOfPartitionEntries <= 0 || h->NumberOfPartitionEntries > 1024) + return false; + + /* overflow check */ + if (h->SizeOfPartitionEntry > SIZE_MAX / h->NumberOfPartitionEntries) + return false; + + return true; +} + +static EFI_STATUS try_gpt( + const EFI_GUID *type, + EFI_BLOCK_IO_PROTOCOL *block_io, + EFI_LBA lba, + EFI_LBA *ret_backup_lba, /* May be changed even on error! */ + HARDDRIVE_DEVICE_PATH *ret_hd) { + + _cleanup_free_ EFI_PARTITION_ENTRY *entries = NULL; + GptHeader gpt; + EFI_STATUS err; + uint32_t crc32; + size_t size; + + assert(block_io); + assert(ret_hd); + + /* Read the GPT header */ + err = block_io->ReadBlocks( + block_io, + block_io->Media->MediaId, + lba, + sizeof(gpt), &gpt); + if (err != EFI_SUCCESS) + return err; + + /* Indicate the location of backup LBA even if the rest of the header is corrupt. */ + if (ret_backup_lba) + *ret_backup_lba = gpt.AlternateLBA; + + if (!verify_gpt(&gpt, lba)) + return EFI_NOT_FOUND; + + /* Now load the GPT entry table */ + size = ALIGN_TO((size_t) gpt.SizeOfPartitionEntry * (size_t) gpt.NumberOfPartitionEntries, 512); + entries = xmalloc(size); + + err = block_io->ReadBlocks( + block_io, + block_io->Media->MediaId, + gpt.PartitionEntryLBA, + size, entries); + if (err != EFI_SUCCESS) + return err; + + /* Calculate CRC of entries array, too */ + err = BS->CalculateCrc32(entries, size, &crc32); + if (err != EFI_SUCCESS || crc32 != gpt.PartitionEntryArrayCRC32) + return EFI_CRC_ERROR; + + /* Now we can finally look for xbootloader partitions. */ + for (size_t i = 0; i < gpt.NumberOfPartitionEntries; i++) { + EFI_PARTITION_ENTRY *entry = + (EFI_PARTITION_ENTRY *) ((uint8_t *) entries + gpt.SizeOfPartitionEntry * i); + + if (!efi_guid_equal(&entry->PartitionTypeGUID, type)) + continue; + + if (entry->EndingLBA < entry->StartingLBA) /* Bogus? */ + continue; + + *ret_hd = (HARDDRIVE_DEVICE_PATH) { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_HARDDRIVE_DP, + .Length = sizeof(HARDDRIVE_DEVICE_PATH), + }, + .PartitionNumber = i + 1, + .PartitionStart = entry->StartingLBA, + .PartitionSize = entry->EndingLBA - entry->StartingLBA + 1, + .MBRType = MBR_TYPE_EFI_PARTITION_TABLE_HEADER, + .SignatureType = SIGNATURE_TYPE_GUID, + }; + memcpy(ret_hd->Signature, &entry->UniquePartitionGUID, sizeof(ret_hd->Signature)); + + return EFI_SUCCESS; + } + + /* This GPT was fully valid, but we didn't find what we are looking for. This + * means there's no reason to check the second copy of the GPT header */ + return EFI_NOT_FOUND; +} + +static EFI_STATUS find_device(const EFI_GUID *type, EFI_HANDLE *device, EFI_DEVICE_PATH **ret_device_path) { + EFI_STATUS err; + + assert(device); + assert(ret_device_path); + + EFI_DEVICE_PATH *partition_path; + err = BS->HandleProtocol(device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &partition_path); + if (err != EFI_SUCCESS) + return err; + + /* Find the (last) partition node itself. */ + EFI_DEVICE_PATH *part_node = NULL; + for (EFI_DEVICE_PATH *node = partition_path; !device_path_is_end(node); + node = device_path_next_node(node)) { + if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_HARDDRIVE_DP) + continue; + + part_node = node; + } + + if (!part_node) + return EFI_NOT_FOUND; + + /* Chop off the partition part, leaving us with the full path to the disk itself. */ + _cleanup_free_ EFI_DEVICE_PATH *disk_path = NULL; + EFI_DEVICE_PATH *p = disk_path = device_path_replace_node(partition_path, part_node, NULL); + + EFI_HANDLE disk_handle; + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), &p, &disk_handle); + if (err != EFI_SUCCESS) + return err; + + /* The drivers for other partitions on this drive may not be initialized on fastboot firmware, so we + * have to ask the firmware to do just that. */ + (void) BS->ConnectController(disk_handle, NULL, NULL, true); + + err = BS->HandleProtocol(disk_handle, MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), (void **) &block_io); + if (err != EFI_SUCCESS) + return err; + + /* Filter out some block devices early. (We only care about block devices that aren't + * partitions themselves — we look for GPT partition tables to parse after all —, and only + * those which contain a medium and have at least 2 blocks.) */ + if (block_io->Media->LogicalPartition || + !block_io->Media->MediaPresent || + block_io->Media->LastBlock <= 1) + return EFI_NOT_FOUND; + + /* Try several copies of the GPT header, in case one is corrupted */ + EFI_LBA backup_lba = 0; + for (size_t nr = 0; nr < 3; nr++) { + EFI_LBA lba; + + /* Read the first copy at LBA 1 and then try the backup GPT header pointed + * to by the first header if that one was corrupted. As a last resort, + * try the very last LBA of this block device. */ + if (nr == 0) + lba = 1; + else if (nr == 1 && backup_lba != 0) + lba = backup_lba; + else if (nr == 2 && backup_lba != block_io->Media->LastBlock) + lba = block_io->Media->LastBlock; + else + continue; + + HARDDRIVE_DEVICE_PATH hd; + err = try_gpt(type, block_io, lba, + nr == 0 ? &backup_lba : NULL, /* Only get backup LBA location from first GPT header. */ + &hd); + if (err != EFI_SUCCESS) { + /* GPT was valid but no XBOOT loader partition found. */ + if (err == EFI_NOT_FOUND) + break; + /* Bad GPT, try next one. */ + continue; + } + + /* Patch in the data we found */ + *ret_device_path = device_path_replace_node(partition_path, part_node, (EFI_DEVICE_PATH *) &hd); + return EFI_SUCCESS; + } + + /* No xbootloader partition found */ + return EFI_NOT_FOUND; +} + +EFI_STATUS partition_open(const EFI_GUID *type, EFI_HANDLE *device, EFI_HANDLE *ret_device, + EFI_FILE **ret_root_dir) { + _cleanup_free_ EFI_DEVICE_PATH *partition_path = NULL; + EFI_HANDLE new_device; + EFI_FILE *root_dir; + EFI_STATUS err; + + assert(type); + assert(device); + assert(ret_root_dir); + + err = find_device(type, device, &partition_path); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *dp = partition_path; + err = BS->LocateDevicePath(MAKE_GUID_PTR(EFI_BLOCK_IO_PROTOCOL), &dp, &new_device); + if (err != EFI_SUCCESS) + return err; + + err = open_volume(new_device, &root_dir); + if (err != EFI_SUCCESS) + return err; + + if (ret_device) + *ret_device = new_device; + *ret_root_dir = root_dir; + return EFI_SUCCESS; +} + +char16_t *disk_get_part_uuid(EFI_HANDLE *handle) { + EFI_STATUS err; + EFI_DEVICE_PATH *dp; + + /* export the device path this image is started from */ + + if (!handle) + return NULL; + + err = BS->HandleProtocol(handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return NULL; + + for (; !device_path_is_end(dp); dp = device_path_next_node(dp)) { + if (dp->Type != MEDIA_DEVICE_PATH || dp->SubType != MEDIA_HARDDRIVE_DP) + continue; + + HARDDRIVE_DEVICE_PATH *hd = (HARDDRIVE_DEVICE_PATH *) dp; + if (hd->SignatureType != SIGNATURE_TYPE_GUID) + continue; + + return xasprintf(GUID_FORMAT_STR, GUID_FORMAT_VAL(hd->SignatureGuid)); + } + + return NULL; +} diff --git a/src/boot/efi/part-discovery.h b/src/boot/efi/part-discovery.h new file mode 100644 index 0000000..bbc87ff --- /dev/null +++ b/src/boot/efi/part-discovery.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define XBOOTLDR_GUID \ + { 0xbc13c2ff, 0x59e6, 0x4262, { 0xa3, 0x52, 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72 } } +#define ESP_GUID \ + { 0xc12a7328, 0xf81f, 0x11d2, { 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b } } + +EFI_STATUS partition_open(const EFI_GUID *type, EFI_HANDLE *device, EFI_HANDLE *ret_device, EFI_FILE **ret_root_dir); +char16_t *disk_get_part_uuid(EFI_HANDLE *handle); diff --git a/src/boot/efi/pe.c b/src/boot/efi/pe.c new file mode 100644 index 0000000..829266b --- /dev/null +++ b/src/boot/efi/pe.c @@ -0,0 +1,332 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "pe.h" +#include "util.h" + +#define DOS_FILE_MAGIC "MZ" +#define PE_FILE_MAGIC "PE\0\0" +#define MAX_SECTIONS 96 + +#if defined(__i386__) +# define TARGET_MACHINE_TYPE 0x014CU +# define TARGET_MACHINE_TYPE_COMPATIBILITY 0x8664U +#elif defined(__x86_64__) +# define TARGET_MACHINE_TYPE 0x8664U +#elif defined(__aarch64__) +# define TARGET_MACHINE_TYPE 0xAA64U +#elif defined(__arm__) +# define TARGET_MACHINE_TYPE 0x01C2U +#elif defined(__riscv) && __riscv_xlen == 32 +# define TARGET_MACHINE_TYPE 0x5032U +#elif defined(__riscv) && __riscv_xlen == 64 +# define TARGET_MACHINE_TYPE 0x5064U +#elif defined(__loongarch__) && __loongarch_grlen == 32 +# define TARGET_MACHINE_TYPE 0x6232U +#elif defined(__loongarch__) && __loongarch_grlen == 64 +# define TARGET_MACHINE_TYPE 0x6264U +#else +# error Unknown EFI arch +#endif + +#ifndef TARGET_MACHINE_TYPE_COMPATIBILITY +# define TARGET_MACHINE_TYPE_COMPATIBILITY 0 +#endif + +typedef struct DosFileHeader { + uint8_t Magic[2]; + uint16_t LastSize; + uint16_t nBlocks; + uint16_t nReloc; + uint16_t HdrSize; + uint16_t MinAlloc; + uint16_t MaxAlloc; + uint16_t ss; + uint16_t sp; + uint16_t Checksum; + uint16_t ip; + uint16_t cs; + uint16_t RelocPos; + uint16_t nOverlay; + uint16_t reserved[4]; + uint16_t OEMId; + uint16_t OEMInfo; + uint16_t reserved2[10]; + uint32_t ExeHeader; +} _packed_ DosFileHeader; + +typedef struct CoffFileHeader { + uint16_t Machine; + uint16_t NumberOfSections; + uint32_t TimeDateStamp; + uint32_t PointerToSymbolTable; + uint32_t NumberOfSymbols; + uint16_t SizeOfOptionalHeader; + uint16_t Characteristics; +} _packed_ CoffFileHeader; + +#define OPTHDR32_MAGIC 0x10B /* PE32 OptionalHeader */ +#define OPTHDR64_MAGIC 0x20B /* PE32+ OptionalHeader */ + +typedef struct PeOptionalHeader { + uint16_t Magic; + uint8_t LinkerMajor; + uint8_t LinkerMinor; + uint32_t SizeOfCode; + uint32_t SizeOfInitializedData; + uint32_t SizeOfUninitializeData; + uint32_t AddressOfEntryPoint; + uint32_t BaseOfCode; + union { + struct { /* PE32 */ + uint32_t BaseOfData; + uint32_t ImageBase32; + }; + uint64_t ImageBase64; /* PE32+ */ + }; + uint32_t SectionAlignment; + uint32_t FileAlignment; + uint16_t MajorOperatingSystemVersion; + uint16_t MinorOperatingSystemVersion; + uint16_t MajorImageVersion; + uint16_t MinorImageVersion; + uint16_t MajorSubsystemVersion; + uint16_t MinorSubsystemVersion; + uint32_t Win32VersionValue; + uint32_t SizeOfImage; + uint32_t SizeOfHeaders; + uint32_t CheckSum; + uint16_t Subsystem; + uint16_t DllCharacteristics; + /* fields with different sizes for 32/64 omitted */ +} _packed_ PeOptionalHeader; + +typedef struct PeFileHeader { + uint8_t Magic[4]; + CoffFileHeader FileHeader; + PeOptionalHeader OptionalHeader; +} _packed_ PeFileHeader; + +typedef struct PeSectionHeader { + uint8_t Name[8]; + uint32_t VirtualSize; + uint32_t VirtualAddress; + uint32_t SizeOfRawData; + uint32_t PointerToRawData; + uint32_t PointerToRelocations; + uint32_t PointerToLinenumbers; + uint16_t NumberOfRelocations; + uint16_t NumberOfLinenumbers; + uint32_t Characteristics; +} _packed_ PeSectionHeader; + +static bool verify_dos(const DosFileHeader *dos) { + assert(dos); + return memcmp(dos->Magic, DOS_FILE_MAGIC, STRLEN(DOS_FILE_MAGIC)) == 0; +} + +static bool verify_pe(const PeFileHeader *pe, bool allow_compatibility) { + assert(pe); + return memcmp(pe->Magic, PE_FILE_MAGIC, STRLEN(PE_FILE_MAGIC)) == 0 && + (pe->FileHeader.Machine == TARGET_MACHINE_TYPE || + (allow_compatibility && pe->FileHeader.Machine == TARGET_MACHINE_TYPE_COMPATIBILITY)) && + pe->FileHeader.NumberOfSections > 0 && + pe->FileHeader.NumberOfSections <= MAX_SECTIONS && + IN_SET(pe->OptionalHeader.Magic, OPTHDR32_MAGIC, OPTHDR64_MAGIC); +} + +static size_t section_table_offset(const DosFileHeader *dos, const PeFileHeader *pe) { + assert(dos); + assert(pe); + return dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader) + pe->FileHeader.SizeOfOptionalHeader; +} + +static void locate_sections( + const PeSectionHeader section_table[], + size_t n_table, + const char * const sections[], + size_t *offsets, + size_t *sizes, + bool in_memory) { + + assert(section_table); + assert(sections); + assert(offsets); + assert(sizes); + + for (size_t i = 0; i < n_table; i++) { + const PeSectionHeader *sect = section_table + i; + + for (size_t j = 0; sections[j]; j++) { + if (memcmp(sect->Name, sections[j], strlen8(sections[j])) != 0) + continue; + + offsets[j] = in_memory ? sect->VirtualAddress : sect->PointerToRawData; + sizes[j] = sect->VirtualSize; + } + } +} + +static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const PeFileHeader *pe) { + size_t addr = 0, size = 0; + static const char *sections[] = { ".compat", NULL }; + + /* The kernel may provide alternative PE entry points for different PE architectures. This allows + * booting a 64-bit kernel on 32-bit EFI that is otherwise running on a 64-bit CPU. The locations of any + * such compat entry points are located in a special PE section. */ + + locate_sections((const PeSectionHeader *) ((const uint8_t *) dos + section_table_offset(dos, pe)), + pe->FileHeader.NumberOfSections, + sections, + &addr, + &size, + /*in_memory=*/true); + + if (size == 0) + return 0; + + typedef struct { + uint8_t type; + uint8_t size; + uint16_t machine_type; + uint32_t entry_point; + } _packed_ LinuxPeCompat1; + + while (size >= sizeof(LinuxPeCompat1) && addr % alignof(LinuxPeCompat1) == 0) { + LinuxPeCompat1 *compat = (LinuxPeCompat1 *) ((uint8_t *) dos + addr); + + if (compat->type == 0 || compat->size == 0 || compat->size > size) + break; + + if (compat->type == 1 && + compat->size >= sizeof(LinuxPeCompat1) && + compat->machine_type == TARGET_MACHINE_TYPE) + return compat->entry_point; + + addr += compat->size; + size -= compat->size; + } + + return 0; +} + +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address) { + assert(base); + assert(ret_compat_address); + + const DosFileHeader *dos = (const DosFileHeader *) base; + if (!verify_dos(dos)) + return EFI_LOAD_ERROR; + + const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader); + if (!verify_pe(pe, /* allow_compatibility= */ true)) + return EFI_LOAD_ERROR; + + /* Support for LINUX_INITRD_MEDIA_GUID was added in kernel stub 1.0. */ + if (pe->OptionalHeader.MajorImageVersion < 1) + return EFI_UNSUPPORTED; + + if (pe->FileHeader.Machine == TARGET_MACHINE_TYPE) { + *ret_compat_address = 0; + return EFI_SUCCESS; + } + + uint32_t compat_address = get_compatibility_entry_address(dos, pe); + if (compat_address == 0) + /* Image type not supported and no compat entry found. */ + return EFI_UNSUPPORTED; + + *ret_compat_address = compat_address; + return EFI_SUCCESS; +} + +EFI_STATUS pe_memory_locate_sections(const void *base, const char * const sections[], size_t *addrs, size_t *sizes) { + const DosFileHeader *dos; + const PeFileHeader *pe; + size_t offset; + + assert(base); + assert(sections); + assert(addrs); + assert(sizes); + + dos = (const DosFileHeader *) base; + if (!verify_dos(dos)) + return EFI_LOAD_ERROR; + + pe = (const PeFileHeader *) ((uint8_t *) base + dos->ExeHeader); + if (!verify_pe(pe, /* allow_compatibility= */ false)) + return EFI_LOAD_ERROR; + + offset = section_table_offset(dos, pe); + locate_sections((PeSectionHeader *) ((uint8_t *) base + offset), + pe->FileHeader.NumberOfSections, + sections, + addrs, + sizes, + /*in_memory=*/true); + + return EFI_SUCCESS; +} + +EFI_STATUS pe_file_locate_sections( + EFI_FILE *dir, + const char16_t *path, + const char * const sections[], + size_t *offsets, + size_t *sizes) { + _cleanup_free_ PeSectionHeader *section_table = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + DosFileHeader dos; + PeFileHeader pe; + size_t len, section_table_len; + EFI_STATUS err; + + assert(dir); + assert(path); + assert(sections); + assert(offsets); + assert(sizes); + + err = dir->Open(dir, &handle, (char16_t *) path, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return err; + + len = sizeof(dos); + err = handle->Read(handle, &len, &dos); + if (err != EFI_SUCCESS) + return err; + if (len != sizeof(dos) || !verify_dos(&dos)) + return EFI_LOAD_ERROR; + + err = handle->SetPosition(handle, dos.ExeHeader); + if (err != EFI_SUCCESS) + return err; + + len = sizeof(pe); + err = handle->Read(handle, &len, &pe); + if (err != EFI_SUCCESS) + return err; + if (len != sizeof(pe) || !verify_pe(&pe, /* allow_compatibility= */ false)) + return EFI_LOAD_ERROR; + + section_table_len = pe.FileHeader.NumberOfSections * sizeof(PeSectionHeader); + section_table = xmalloc(section_table_len); + if (!section_table) + return EFI_OUT_OF_RESOURCES; + + err = handle->SetPosition(handle, section_table_offset(&dos, &pe)); + if (err != EFI_SUCCESS) + return err; + + len = section_table_len; + err = handle->Read(handle, &len, section_table); + if (err != EFI_SUCCESS) + return err; + if (len != section_table_len) + return EFI_LOAD_ERROR; + + locate_sections(section_table, pe.FileHeader.NumberOfSections, + sections, offsets, sizes, /*in_memory=*/false); + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/pe.h b/src/boot/efi/pe.h new file mode 100644 index 0000000..7e2258f --- /dev/null +++ b/src/boot/efi/pe.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS pe_memory_locate_sections( + const void *base, + const char * const sections[], + size_t *addrs, + size_t *sizes); + +EFI_STATUS pe_file_locate_sections( + EFI_FILE *dir, + const char16_t *path, + const char * const sections[], + size_t *offsets, + size_t *sizes); + +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address); diff --git a/src/boot/efi/proto/block-io.h b/src/boot/efi/proto/block-io.h new file mode 100644 index 0000000..e977f70 --- /dev/null +++ b/src/boot/efi/proto/block-io.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_BLOCK_IO_PROTOCOL_GUID \ + GUID_DEF(0x0964e5b21, 0x6459, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +typedef struct EFI_BLOCK_IO_PROTOCOL EFI_BLOCK_IO_PROTOCOL; +struct EFI_BLOCK_IO_PROTOCOL { + uint64_t Revision; + struct { + uint32_t MediaId; + bool RemovableMedia; + bool MediaPresent; + bool LogicalPartition; + bool ReadOnly; + bool WriteCaching; + uint32_t BlockSize; + uint32_t IoAlign; + EFI_LBA LastBlock; + EFI_LBA LowestAlignedLba; + uint32_t LogicalBlocksPerPhysicalBlock; + uint32_t OptimalTransferLengthGranularity; + } *Media; + + EFI_STATUS (EFIAPI *Reset)( + EFI_BLOCK_IO_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadBlocks)( + EFI_BLOCK_IO_PROTOCOL *This, + uint32_t MediaId, + EFI_LBA LBA, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *WriteBlocks)( + EFI_BLOCK_IO_PROTOCOL *This, + uint32_t MediaId, + EFI_LBA LBA, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *FlushBlocks)(EFI_BLOCK_IO_PROTOCOL *This); +}; diff --git a/src/boot/efi/proto/console-control.h b/src/boot/efi/proto/console-control.h new file mode 100644 index 0000000..d3a92ea --- /dev/null +++ b/src/boot/efi/proto/console-control.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_CONSOLE_CONTROL_PROTOCOL_GUID \ + GUID_DEF(0xf42f7782, 0x12e, 0x4c12, 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21) + +typedef enum { + EfiConsoleControlScreenText, + EfiConsoleControlScreenGraphics, + EfiConsoleControlScreenMaxValue, +} EFI_CONSOLE_CONTROL_SCREEN_MODE; + +typedef struct EFI_CONSOLE_CONTROL_PROTOCOL EFI_CONSOLE_CONTROL_PROTOCOL; +struct EFI_CONSOLE_CONTROL_PROTOCOL { + EFI_STATUS (EFIAPI *GetMode)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE *Mode, + bool *UgaExists, + bool *StdInLocked); + EFI_STATUS (EFIAPI *SetMode)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE Mode); + EFI_STATUS(EFIAPI *LockStdIn)( + EFI_CONSOLE_CONTROL_PROTOCOL *This, + char16_t *Password); +}; diff --git a/src/boot/efi/proto/device-path.h b/src/boot/efi/proto/device-path.h new file mode 100644 index 0000000..0fabae1 --- /dev/null +++ b/src/boot/efi/proto/device-path.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_DEVICE_PATH_PROTOCOL_GUID \ + GUID_DEF(0x09576e91, 0x6d3f, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_DEVICE_PATH_TO_TEXT_PROTOCOL_GUID \ + GUID_DEF(0x8b843e20, 0x8132, 0x4852, 0x90, 0xcc, 0x55, 0x1a, 0x4e, 0x4a, 0x7f, 0x1c) +#define EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL_GUID \ + GUID_DEF(0x05c99a21, 0xc70f, 0x4ad2, 0x8a, 0x5f, 0x35, 0xdf, 0x33, 0x43, 0xf5, 0x1e) + +/* Device path types. */ +enum { + HARDWARE_DEVICE_PATH = 0x01, + ACPI_DEVICE_PATH = 0x02, + MESSAGING_DEVICE_PATH = 0x03, + MEDIA_DEVICE_PATH = 0x04, + BBS_DEVICE_PATH = 0x05, + END_DEVICE_PATH_TYPE = 0x7f, +}; + +/* Device path sub-types. */ +enum { + END_INSTANCE_DEVICE_PATH_SUBTYPE = 0x01, + END_ENTIRE_DEVICE_PATH_SUBTYPE = 0xff, + + MEDIA_HARDDRIVE_DP = 0x01, + MEDIA_VENDOR_DP = 0x03, + MEDIA_FILEPATH_DP = 0x04, + MEDIA_PIWG_FW_FILE_DP = 0x06, + MEDIA_PIWG_FW_VOL_DP = 0x07, +}; + +struct _packed_ EFI_DEVICE_PATH_PROTOCOL { + uint8_t Type; + uint8_t SubType; + uint16_t Length; +}; + +typedef struct { + EFI_DEVICE_PATH Header; + EFI_GUID Guid; +} _packed_ VENDOR_DEVICE_PATH; + +#define MBR_TYPE_PCAT 0x01U +#define MBR_TYPE_EFI_PARTITION_TABLE_HEADER 0x02U +#define NO_DISK_SIGNATURE 0x00U +#define SIGNATURE_TYPE_MBR 0x01U +#define SIGNATURE_TYPE_GUID 0x02U + +typedef struct { + EFI_DEVICE_PATH Header; + uint32_t PartitionNumber; + uint64_t PartitionStart; + uint64_t PartitionSize; + union { + uint8_t Signature[16]; + EFI_GUID SignatureGuid; + }; + uint8_t MBRType; + uint8_t SignatureType; +} _packed_ HARDDRIVE_DEVICE_PATH; + +typedef struct { + EFI_DEVICE_PATH Header; + char16_t PathName[]; +} _packed_ FILEPATH_DEVICE_PATH; + +typedef struct { + char16_t* (EFIAPI *ConvertDeviceNodeToText)( + const EFI_DEVICE_PATH *DeviceNode, + bool DisplayOnly, + bool AllowShortcuts); + char16_t* (EFIAPI *ConvertDevicePathToText)( + const EFI_DEVICE_PATH *DevicePath, + bool DisplayOnly, + bool AllowShortcuts); +} EFI_DEVICE_PATH_TO_TEXT_PROTOCOL; + +typedef struct { + EFI_DEVICE_PATH* (EFIAPI *ConvertTextToDevicNode)( + const char16_t *TextDeviceNode); + EFI_DEVICE_PATH* (EFIAPI *ConvertTextToDevicPath)( + const char16_t *ConvertTextToDevicPath); +} EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL; diff --git a/src/boot/efi/proto/dt-fixup.h b/src/boot/efi/proto/dt-fixup.h new file mode 100644 index 0000000..6edbef5 --- /dev/null +++ b/src/boot/efi/proto/dt-fixup.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_DTB_TABLE_GUID \ + GUID_DEF(0xb1b621d5, 0xf19c, 0x41a5, 0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0) +#define EFI_DT_FIXUP_PROTOCOL_GUID \ + GUID_DEF(0xe617d64c, 0xfe08, 0x46da, 0xf4, 0xdc, 0xbb, 0xd5, 0x87, 0x0c, 0x73, 0x00) + +#define EFI_DT_FIXUP_PROTOCOL_REVISION 0x00010000 + +/* Add nodes and update properties */ +#define EFI_DT_APPLY_FIXUPS 0x00000001 + +/* + * Reserve memory according to the /reserved-memory node + * and the memory reservation block + */ +#define EFI_DT_RESERVE_MEMORY 0x00000002 + +typedef struct EFI_DT_FIXUP_PROTOCOL EFI_DT_FIXUP_PROTOCOL; +struct EFI_DT_FIXUP_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *Fixup)( + EFI_DT_FIXUP_PROTOCOL *This, + void *Fdt, + size_t *BufferSize, + uint32_t Flags); +}; diff --git a/src/boot/efi/proto/file-io.h b/src/boot/efi/proto/file-io.h new file mode 100644 index 0000000..001ad48 --- /dev/null +++ b/src/boot/efi/proto/file-io.h @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \ + GUID_DEF(0x0964e5b22, 0x6459, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_FILE_INFO_ID \ + GUID_DEF(0x009576e92, 0x6d3f, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +#define EFI_FILE_MODE_READ 0x0000000000000001U +#define EFI_FILE_MODE_WRITE 0x0000000000000002U +#define EFI_FILE_MODE_CREATE 0x8000000000000000U + +#define EFI_FILE_READ_ONLY 0x01U +#define EFI_FILE_HIDDEN 0x02U +#define EFI_FILE_SYSTEM 0x04U +#define EFI_FILE_RESERVED 0x08U +#define EFI_FILE_DIRECTORY 0x10U +#define EFI_FILE_ARCHIVE 0x20U +#define EFI_FILE_VALID_ATTR 0x37U + +typedef struct { + uint64_t Size; + uint64_t FileSize; + uint64_t PhysicalSize; + EFI_TIME CreateTime; + EFI_TIME LastAccessTime; + EFI_TIME ModificationTime; + uint64_t Attribute; + char16_t FileName[]; +} EFI_FILE_INFO; + +/* Some broken firmware violates the EFI spec by still advancing the readdir + * position when returning EFI_BUFFER_TOO_SMALL, effectively skipping over any files when + * the buffer was too small. Therefore, we always start with a buffer that should handle FAT32 + * max file name length. */ +#define EFI_FILE_INFO_MIN_SIZE (offsetof(EFI_FILE_INFO, FileName) + 256U * sizeof(char16_t)) + +typedef struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL EFI_SIMPLE_FILE_SYSTEM_PROTOCOL; +struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *OpenVolume)( + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This, + EFI_FILE **Root); +}; + +struct EFI_FILE_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *Open)( + EFI_FILE *This, + EFI_FILE **NewHandle, + char16_t *FileName, + uint64_t OpenMode, + uint64_t Attributes); + EFI_STATUS (EFIAPI *Close)(EFI_FILE *This); + EFI_STATUS (EFIAPI *Delete)(EFI_FILE *This); + EFI_STATUS (EFIAPI *Read)( + EFI_FILE *This, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *Write)( + EFI_FILE *This, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *GetPosition)(EFI_FILE *This, uint64_t *Position); + EFI_STATUS (EFIAPI *SetPosition)(EFI_FILE *This, uint64_t Position); + EFI_STATUS (EFIAPI *GetInfo)( + EFI_FILE *This, + EFI_GUID *InformationType, + size_t *BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *SetInfo)( + EFI_FILE *This, + EFI_GUID *InformationType, + size_t BufferSize, + void *Buffer); + EFI_STATUS (EFIAPI *Flush)(EFI_FILE *This); + void *OpenEx; + void *ReadEx; + void *WriteEx; + void *FlushEx; +}; diff --git a/src/boot/efi/proto/graphics-output.h b/src/boot/efi/proto/graphics-output.h new file mode 100644 index 0000000..f49e580 --- /dev/null +++ b/src/boot/efi/proto/graphics-output.h @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \ + GUID_DEF(0x9042a9de, 0x23dc, 0x4a38, 0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a) + +typedef enum { + PixelRedGreenBlueReserved8BitPerColor, + PixelBlueGreenRedReserved8BitPerColor, + PixelBitMask, + PixelBltOnly, + PixelFormatMax, +} EFI_GRAPHICS_PIXEL_FORMAT; + +typedef enum { + EfiBltVideoFill, + EfiBltVideoToBltBuffer, + EfiBltBufferToVideo, + EfiBltVideoToVideo, + EfiGraphicsOutputBltOperationMax, +} EFI_GRAPHICS_OUTPUT_BLT_OPERATION; + +typedef struct { + uint32_t RedMask; + uint32_t GreenMask; + uint32_t BlueMask; + uint32_t ReservedMask; +} EFI_PIXEL_BITMASK; + +typedef struct { + uint8_t Blue; + uint8_t Green; + uint8_t Red; + uint8_t Reserved; +} EFI_GRAPHICS_OUTPUT_BLT_PIXEL; + +typedef struct { + uint32_t Version; + uint32_t HorizontalResolution; + uint32_t VerticalResolution; + EFI_GRAPHICS_PIXEL_FORMAT PixelFormat; + EFI_PIXEL_BITMASK PixelInformation; + uint32_t PixelsPerScanLine; +} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION; + +typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL EFI_GRAPHICS_OUTPUT_PROTOCOL; +struct EFI_GRAPHICS_OUTPUT_PROTOCOL { + EFI_STATUS (EFIAPI *QueryMode)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + uint32_t ModeNumber, + size_t *SizeOfInfo, + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info); + EFI_STATUS(EFIAPI *SetMode)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + uint32_t ModeNumber); + EFI_STATUS (EFIAPI *Blt)( + EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer, + EFI_GRAPHICS_OUTPUT_BLT_OPERATION BltOperation, + size_t SourceX, + size_t SourceY, + size_t DestinationX, + size_t DestinationY, + size_t Width, + size_t Height, + size_t Delta); + + struct { + uint32_t MaxMode; + uint32_t Mode; + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info; + size_t SizeOfInfo; + EFI_PHYSICAL_ADDRESS FrameBufferBase; + size_t FrameBufferSize; + } *Mode; +}; diff --git a/src/boot/efi/proto/load-file.h b/src/boot/efi/proto/load-file.h new file mode 100644 index 0000000..2e01ce5 --- /dev/null +++ b/src/boot/efi/proto/load-file.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_LOAD_FILE_PROTOCOL_GUID \ + GUID_DEF(0x56EC3091, 0x954C, 0x11d2, 0x8e, 0x3f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_LOAD_FILE2_PROTOCOL_GUID \ + GUID_DEF(0x4006c0c1, 0xfcb3, 0x403e, 0x99, 0x6d, 0x4a, 0x6c, 0x87, 0x24, 0xe0, 0x6d) + +typedef struct EFI_LOAD_FILE_PROTOCOL EFI_LOAD_FILE_PROTOCOL; +typedef EFI_LOAD_FILE_PROTOCOL EFI_LOAD_FILE2_PROTOCOL; + +struct EFI_LOAD_FILE_PROTOCOL { + EFI_STATUS (EFIAPI *LoadFile)( + EFI_LOAD_FILE_PROTOCOL *This, + EFI_DEVICE_PATH *FilePath, + bool BootPolicy, + size_t *BufferSize, + void *Buffer); +}; diff --git a/src/boot/efi/proto/loaded-image.h b/src/boot/efi/proto/loaded-image.h new file mode 100644 index 0000000..46371e7 --- /dev/null +++ b/src/boot/efi/proto/loaded-image.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_LOADED_IMAGE_PROTOCOL_GUID \ + GUID_DEF(0x5B1B31A1, 0x9562, 0x11d2, 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B) +#define EFI_LOADED_IMAGE_DEVICE_PATH_PROTOCOL_GUID \ + GUID_DEF(0xbc62157e, 0x3e33, 0x4fec, 0x99, 0x20, 0x2d, 0x3b, 0x36, 0xd7, 0x50, 0xdf) + +typedef EFI_STATUS (EFIAPI *EFI_IMAGE_ENTRY_POINT)( + EFI_HANDLE ImageHandle, + EFI_SYSTEM_TABLE *SystemTable); + +typedef struct { + uint32_t Revision; + EFI_HANDLE ParentHandle; + EFI_SYSTEM_TABLE *SystemTable; + EFI_HANDLE DeviceHandle; + EFI_DEVICE_PATH *FilePath; + void *Reserved; + uint32_t LoadOptionsSize; + void *LoadOptions; + void *ImageBase; + uint64_t ImageSize; + EFI_MEMORY_TYPE ImageCodeType; + EFI_MEMORY_TYPE ImageDataType; + EFI_STATUS (EFIAPI *Unload)(EFI_HANDLE ImageHandle); +} EFI_LOADED_IMAGE_PROTOCOL; diff --git a/src/boot/efi/proto/rng.h b/src/boot/efi/proto/rng.h new file mode 100644 index 0000000..8ed1fd4 --- /dev/null +++ b/src/boot/efi/proto/rng.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_RNG_PROTOCOL_GUID \ + GUID_DEF(0x3152bca5, 0xeade, 0x433d, 0x86, 0x2e, 0xc0, 0x1c, 0xdc, 0x29, 0x1f, 0x44) + +typedef struct EFI_RNG_PROTOCOL EFI_RNG_PROTOCOL; +struct EFI_RNG_PROTOCOL { + EFI_STATUS (EFIAPI *GetInfo)( + EFI_RNG_PROTOCOL *This, + size_t *RNGAlgorithmListSize, + EFI_GUID *RNGAlgorithmList); + EFI_STATUS (EFIAPI *GetRNG)( + EFI_RNG_PROTOCOL *This, + EFI_GUID *RNGAlgorithm, + size_t RNGValueLength, + uint8_t *RNGValue); +}; diff --git a/src/boot/efi/proto/security-arch.h b/src/boot/efi/proto/security-arch.h new file mode 100644 index 0000000..2675c61 --- /dev/null +++ b/src/boot/efi/proto/security-arch.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SECURITY_ARCH_PROTOCOL_GUID \ + GUID_DEF(0xA46423E3, 0x4617, 0x49f1, 0xB9, 0xFF, 0xD1, 0xBF, 0xA9, 0x11, 0x58, 0x39) +#define EFI_SECURITY2_ARCH_PROTOCOL_GUID \ + GUID_DEF(0x94ab2f58, 0x1438, 0x4ef1, 0x91, 0x52, 0x18, 0x94, 0x1a, 0x3a, 0x0e, 0x68) + +typedef struct EFI_SECURITY_ARCH_PROTOCOL EFI_SECURITY_ARCH_PROTOCOL; +typedef struct EFI_SECURITY2_ARCH_PROTOCOL EFI_SECURITY2_ARCH_PROTOCOL; + +typedef EFI_STATUS (EFIAPI *EFI_SECURITY_FILE_AUTHENTICATION_STATE)( + const EFI_SECURITY_ARCH_PROTOCOL *This, + uint32_t AuthenticationStatus, + const EFI_DEVICE_PATH *File); + +typedef EFI_STATUS (EFIAPI *EFI_SECURITY2_FILE_AUTHENTICATION)( + const EFI_SECURITY2_ARCH_PROTOCOL *This, + const EFI_DEVICE_PATH *DevicePath, + void *FileBuffer, + size_t FileSize, + bool BootPolicy); + +struct EFI_SECURITY_ARCH_PROTOCOL { + EFI_SECURITY_FILE_AUTHENTICATION_STATE FileAuthenticationState; +}; + +struct EFI_SECURITY2_ARCH_PROTOCOL { + EFI_SECURITY2_FILE_AUTHENTICATION FileAuthentication; +}; diff --git a/src/boot/efi/proto/shell-parameters.h b/src/boot/efi/proto/shell-parameters.h new file mode 100644 index 0000000..8080922 --- /dev/null +++ b/src/boot/efi/proto/shell-parameters.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SHELL_PARAMETERS_PROTOCOL_GUID \ + GUID_DEF(0x752f3136, 0x4e16, 0x4fdc, 0xa2, 0x2a, 0xe5, 0xf4, 0x68, 0x12, 0xf4, 0xca) + +typedef struct { + char16_t **Argv; + size_t Argc; + void *StdIn; + void *StdOut; + void *StdErr; +} EFI_SHELL_PARAMETERS_PROTOCOL; diff --git a/src/boot/efi/proto/simple-text-io.h b/src/boot/efi/proto/simple-text-io.h new file mode 100644 index 0000000..95016d3 --- /dev/null +++ b/src/boot/efi/proto/simple-text-io.h @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID \ + GUID_DEF(0x387477c1, 0x69c7, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) +#define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \ + GUID_DEF(0xdd9e7534, 0x7762, 0x4698, 0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa) +#define EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID \ + GUID_DEF(0x387477c2, 0x69c7, 0x11d2, 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b) + +#define EFI_SHIFT_STATE_VALID 0x80000000U +#define EFI_RIGHT_SHIFT_PRESSED 0x00000001U +#define EFI_LEFT_SHIFT_PRESSED 0x00000002U +#define EFI_RIGHT_CONTROL_PRESSED 0x00000004U +#define EFI_LEFT_CONTROL_PRESSED 0x00000008U +#define EFI_RIGHT_ALT_PRESSED 0x00000010U +#define EFI_LEFT_ALT_PRESSED 0x00000020U +#define EFI_RIGHT_LOGO_PRESSED 0x00000040U +#define EFI_LEFT_LOGO_PRESSED 0x00000080U +#define EFI_MENU_KEY_PRESSED 0x00000100U +#define EFI_SYS_REQ_PRESSED 0x00000200U + +#define EFI_TOGGLE_STATE_VALID 0x80U +#define EFI_KEY_STATE_EXPOSED 0x40U +#define EFI_SCROLL_LOCK_ACTIVE 0x01U +#define EFI_NUM_LOCK_ACTIVE 0x02U +#define EFI_CAPS_LOCK_ACTIVE 0x04U + +enum { + EFI_BLACK = 0x00, + EFI_BLUE = 0x01, + EFI_GREEN = 0x02, + EFI_CYAN = EFI_BLUE | EFI_GREEN, + EFI_RED = 0x04, + EFI_MAGENTA = EFI_BLUE | EFI_RED, + EFI_BROWN = EFI_GREEN | EFI_RED, + EFI_LIGHTGRAY = EFI_BLUE | EFI_GREEN | EFI_RED, + EFI_BRIGHT = 0x08, + EFI_DARKGRAY = EFI_BLACK | EFI_BRIGHT, + EFI_LIGHTBLUE = EFI_BLUE | EFI_BRIGHT, + EFI_LIGHTGREEN = EFI_GREEN | EFI_BRIGHT, + EFI_LIGHTCYAN = EFI_CYAN | EFI_BRIGHT, + EFI_LIGHTRED = EFI_RED | EFI_BRIGHT, + EFI_LIGHTMAGENTA = EFI_MAGENTA | EFI_BRIGHT, + EFI_YELLOW = EFI_BROWN | EFI_BRIGHT, + EFI_WHITE = EFI_BLUE | EFI_GREEN | EFI_RED | EFI_BRIGHT, +}; + +#define EFI_TEXT_ATTR(fg, bg) ((fg) | ((bg) << 4)) +#define EFI_TEXT_ATTR_SWAP(c) EFI_TEXT_ATTR(((c) & 0xF0U) >> 4, (c) & 0xFU) + +enum { + SCAN_NULL = 0x000, + SCAN_UP = 0x001, + SCAN_DOWN = 0x002, + SCAN_RIGHT = 0x003, + SCAN_LEFT = 0x004, + SCAN_HOME = 0x005, + SCAN_END = 0x006, + SCAN_INSERT = 0x007, + SCAN_DELETE = 0x008, + SCAN_PAGE_UP = 0x009, + SCAN_PAGE_DOWN = 0x00A, + SCAN_F1 = 0x00B, + SCAN_F2 = 0x00C, + SCAN_F3 = 0x00D, + SCAN_F4 = 0x00E, + SCAN_F5 = 0x00F, + SCAN_F6 = 0x010, + SCAN_F7 = 0x011, + SCAN_F8 = 0x012, + SCAN_F9 = 0x013, + SCAN_F10 = 0x014, + SCAN_F11 = 0x015, + SCAN_F12 = 0x016, + SCAN_ESC = 0x017, + SCAN_PAUSE = 0x048, + SCAN_F13 = 0x068, + SCAN_F14 = 0x069, + SCAN_F15 = 0x06A, + SCAN_F16 = 0x06B, + SCAN_F17 = 0x06C, + SCAN_F18 = 0x06D, + SCAN_F19 = 0x06E, + SCAN_F20 = 0x06F, + SCAN_F21 = 0x070, + SCAN_F22 = 0x071, + SCAN_F23 = 0x072, + SCAN_F24 = 0x073, + SCAN_MUTE = 0x07F, + SCAN_VOLUME_UP = 0x080, + SCAN_VOLUME_DOWN = 0x081, + SCAN_BRIGHTNESS_UP = 0x100, + SCAN_BRIGHTNESS_DOWN = 0x101, + SCAN_SUSPEND = 0x102, + SCAN_HIBERNATE = 0x103, + SCAN_TOGGLE_DISPLAY = 0x104, + SCAN_RECOVERY = 0x105, + SCAN_EJECT = 0x106, +}; + +typedef struct { + uint16_t ScanCode; + char16_t UnicodeChar; +} EFI_INPUT_KEY; + +typedef struct { + uint32_t KeyShiftState; + uint8_t KeyToggleState; +} EFI_KEY_STATE; + +typedef struct { + EFI_INPUT_KEY Key; + EFI_KEY_STATE KeyState; +} EFI_KEY_DATA; + +struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadKeyStroke)( + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, + EFI_INPUT_KEY *Key); + EFI_EVENT WaitForKey; +}; + +typedef struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; +struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *ReadKeyStrokeEx)( + EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + EFI_KEY_DATA *KeyData); + EFI_EVENT WaitForKeyEx; + void *SetState; + void *RegisterKeyNotify; + void *UnregisterKeyNotify; +}; + +typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; +struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { + EFI_STATUS (EFIAPI *Reset)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + bool ExtendedVerification); + EFI_STATUS (EFIAPI *OutputString)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + char16_t *String); + EFI_STATUS (EFIAPI *TestString)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + char16_t *String); + EFI_STATUS (EFIAPI *QueryMode)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t ModeNumber, + size_t *Columns, + size_t *Rows); + EFI_STATUS (EFIAPI *SetMode)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t ModeNumber); + EFI_STATUS (EFIAPI *SetAttribute)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t Attribute); + EFI_STATUS (EFIAPI *ClearScreen)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This); + EFI_STATUS (EFIAPI *SetCursorPosition)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + size_t Column, + size_t Row); + EFI_STATUS (EFIAPI *EnableCursor)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, + bool Visible); + struct { + int32_t MaxMode; + int32_t Mode; + int32_t Attribute; + int32_t CursorColumn; + int32_t CursorRow; + bool CursorVisible; + } *Mode; +}; diff --git a/src/boot/efi/proto/tcg.h b/src/boot/efi/proto/tcg.h new file mode 100644 index 0000000..b4b8296 --- /dev/null +++ b/src/boot/efi/proto/tcg.h @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_TCG_PROTOCOL_GUID \ + GUID_DEF(0xf541796d, 0xa62e, 0x4954, 0xa7, 0x75, 0x95, 0x84, 0xf6, 0x1b, 0x9c, 0xdd) +#define EFI_TCG2_PROTOCOL_GUID \ + GUID_DEF(0x607f766c, 0x7455, 0x42be, 0x93, 0x0b, 0xe4, 0xd7, 0x6d, 0xb2, 0x72, 0x0f) + +#define TCG_ALG_SHA 0x4 +#define EFI_TCG2_EVENT_HEADER_VERSION 1 +#define EV_IPL 13 +#define EV_EVENT_TAG UINT32_C(6) + +typedef struct { + uint8_t Major; + uint8_t Minor; + uint8_t RevMajor; + uint8_t RevMinor; +} TCG_VERSION; + +typedef struct { + uint8_t Major; + uint8_t Minor; +} EFI_TCG2_VERSION; + +typedef struct { + uint8_t Size; + TCG_VERSION StructureVersion; + TCG_VERSION ProtocolSpecVersion; + uint8_t HashAlgorithmBitmap; + bool TPMPresentFlag; + bool TPMDeactivatedFlag; +} EFI_TCG_BOOT_SERVICE_CAPABILITY; + +typedef struct { + uint8_t Size; + EFI_TCG2_VERSION StructureVersion; + EFI_TCG2_VERSION ProtocolVersion; + uint32_t HashAlgorithmBitmap; + uint32_t SupportedEventLogs; + bool TPMPresentFlag; + uint16_t MaxCommandSize; + uint16_t MaxResponseSize; + uint32_t ManufacturerID; + uint32_t NumberOfPCRBanks; + uint32_t ActivePcrBanks; +} EFI_TCG2_BOOT_SERVICE_CAPABILITY; + +typedef struct { + uint32_t PCRIndex; + uint32_t EventType; + struct { + uint8_t Digest[20]; + } Digest; + uint32_t EventSize; + uint8_t Event[]; +} _packed_ TCG_PCR_EVENT; + +typedef struct { + uint32_t HeaderSize; + uint16_t HeaderVersion; + uint32_t PCRIndex; + uint32_t EventType; +} _packed_ EFI_TCG2_EVENT_HEADER; + +typedef struct { + uint32_t Size; + EFI_TCG2_EVENT_HEADER Header; + uint8_t Event[]; +} _packed_ EFI_TCG2_EVENT; + +typedef struct { + uint32_t EventId; + uint32_t EventSize; + uint8_t Event[]; +} _packed_ EFI_TCG2_TAGGED_EVENT; + +typedef struct EFI_TCG_PROTOCOL EFI_TCG_PROTOCOL; +struct EFI_TCG_PROTOCOL { + EFI_STATUS (EFIAPI *StatusCheck)( + EFI_TCG_PROTOCOL *This, + EFI_TCG_BOOT_SERVICE_CAPABILITY *ProtocolCapability, + uint32_t *TCGFeatureFlags, + EFI_PHYSICAL_ADDRESS *EventLogLocation, + EFI_PHYSICAL_ADDRESS *EventLogLastEntry); + void *HashAll; + void *LogEvent; + void *PassThroughToTpm; + EFI_STATUS (EFIAPI *HashLogExtendEvent)( + EFI_TCG_PROTOCOL *This, + EFI_PHYSICAL_ADDRESS HashData, + uint64_t HashDataLen, + uint32_t AlgorithmId, + TCG_PCR_EVENT *TCGLogData, + uint32_t *EventNumber, + EFI_PHYSICAL_ADDRESS *EventLogLastEntry); +}; + +typedef struct EFI_TCG2_PROTOCOL EFI_TCG2_PROTOCOL; +struct EFI_TCG2_PROTOCOL { + EFI_STATUS (EFIAPI *GetCapability)( + EFI_TCG2_PROTOCOL *This, + EFI_TCG2_BOOT_SERVICE_CAPABILITY *ProtocolCapability); + void *GetEventLog; + EFI_STATUS (EFIAPI *HashLogExtendEvent)( + EFI_TCG2_PROTOCOL *This, + uint64_t Flags, + EFI_PHYSICAL_ADDRESS DataToHash, + uint64_t DataToHashLen, + EFI_TCG2_EVENT *EfiTcgEvent); + void *SubmitCommand; + void *GetActivePcrBanks; + void *SetActivePcrBanks; + void *GetResultOfSetActivePcrBanks; +}; diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c new file mode 100644 index 0000000..8147e54 --- /dev/null +++ b/src/boot/efi/random-seed.c @@ -0,0 +1,325 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memory-util-fundamental.h" +#include "proto/rng.h" +#include "random-seed.h" +#include "secure-boot.h" +#include "sha256.h" +#include "util.h" + +#define RANDOM_MAX_SIZE_MIN (32U) +#define RANDOM_MAX_SIZE_MAX (32U*1024U) + +struct linux_efi_random_seed { + uint32_t size; + uint8_t seed[]; +}; + +#define LINUX_EFI_RANDOM_SEED_TABLE_GUID \ + { 0x1ce1e5bc, 0x7ceb, 0x42f2, { 0x81, 0xe5, 0x8a, 0xad, 0xf1, 0x80, 0xf5, 0x7b } } + +/* SHA256 gives us 256/8=32 bytes */ +#define HASH_VALUE_SIZE 32 + +/* Linux's RNG is 256 bits, so let's provide this much */ +#define DESIRED_SEED_SIZE 32 + +/* Some basic domain separation in case somebody uses this data elsewhere */ +#define HASH_LABEL "systemd-boot random seed label v1" + +static EFI_STATUS acquire_rng(void *ret, size_t size) { + EFI_RNG_PROTOCOL *rng; + EFI_STATUS err; + + assert(ret); + + /* Try to acquire the specified number of bytes from the UEFI RNG */ + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_RNG_PROTOCOL), NULL, (void **) &rng); + if (err != EFI_SUCCESS) + return err; + if (!rng) + return EFI_UNSUPPORTED; + + err = rng->GetRNG(rng, NULL, size, ret); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to acquire RNG data: %m"); + return EFI_SUCCESS; +} + +static EFI_STATUS acquire_system_token(void **ret, size_t *ret_size) { + _cleanup_free_ char *data = NULL; + EFI_STATUS err; + size_t size; + + assert(ret); + assert(ret_size); + + err = efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderSystemToken", &data, &size); + if (err != EFI_SUCCESS) { + if (err != EFI_NOT_FOUND) + log_error_status(err, "Failed to read LoaderSystemToken EFI variable: %m"); + return err; + } + + if (size <= 0) + return log_error_status(EFI_NOT_FOUND, "System token too short, ignoring."); + + *ret = TAKE_PTR(data); + *ret_size = size; + + return EFI_SUCCESS; +} + +static void validate_sha256(void) { + +#ifdef EFI_DEBUG + /* Let's validate our SHA256 implementation. We stole it from glibc, and converted it to UEFI + * style. We better check whether it does the right stuff. We use the simpler test vectors from the + * SHA spec. Note that we strip this out in optimization builds. */ + + static const struct { + const char *string; + uint8_t hash[HASH_VALUE_SIZE]; + } array[] = { + { "abc", + { 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad }}, + + { "", + { 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }}, + + { "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + { 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, + 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39, + 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67, + 0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1 }}, + + { "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + { 0xcf, 0x5b, 0x16, 0xa7, 0x78, 0xaf, 0x83, 0x80, + 0x03, 0x6c, 0xe5, 0x9e, 0x7b, 0x04, 0x92, 0x37, + 0x0b, 0x24, 0x9b, 0x11, 0xe8, 0xf0, 0x7a, 0x51, + 0xaf, 0xac, 0x45, 0x03, 0x7a, 0xfe, 0xe9, 0xd1 }}, + }; + + for (size_t i = 0; i < ELEMENTSOF(array); i++) + assert(memcmp(SHA256_DIRECT(array[i].string, strlen8(array[i].string)), array[i].hash, HASH_VALUE_SIZE) == 0); +#endif +} + +EFI_STATUS process_random_seed(EFI_FILE *root_dir) { + uint8_t random_bytes[DESIRED_SEED_SIZE], hash_key[HASH_VALUE_SIZE]; + _cleanup_free_ struct linux_efi_random_seed *new_seed_table = NULL; + struct linux_efi_random_seed *previous_seed_table = NULL; + _cleanup_free_ void *seed = NULL, *system_token = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + _cleanup_free_ EFI_FILE_INFO *info = NULL; + struct sha256_ctx hash; + uint64_t uefi_monotonic_counter = 0; + size_t size, rsize, wsize; + bool seeded_by_efi = false; + EFI_STATUS err; + EFI_TIME now; + + CLEANUP_ERASE(random_bytes); + CLEANUP_ERASE(hash_key); + CLEANUP_ERASE(hash); + + assert(root_dir); + assert_cc(DESIRED_SEED_SIZE == HASH_VALUE_SIZE); + + validate_sha256(); + + /* hash = LABEL || sizeof(input1) || input1 || ... || sizeof(inputN) || inputN */ + sha256_init_ctx(&hash); + + /* Some basic domain separation in case somebody uses this data elsewhere */ + sha256_process_bytes(HASH_LABEL, sizeof(HASH_LABEL) - 1, &hash); + + previous_seed_table = find_configuration_table(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE)); + if (!previous_seed_table) { + size = 0; + sha256_process_bytes(&size, sizeof(size), &hash); + } else { + size = previous_seed_table->size; + seeded_by_efi = size >= DESIRED_SEED_SIZE; + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(previous_seed_table->seed, size, &hash); + + /* Zero and free the previous seed table only at the end after we've managed to install a new + * one, so that in case this function fails or aborts, Linux still receives whatever the + * previous bootloader chain set. So, the next line of this block is not an explicit_bzero() + * call. */ + } + + /* Request some random data from the UEFI RNG. We don't need this to work safely, but it's a good + * idea to use it because it helps us for cases where users mistakenly include a random seed in + * golden master images that are replicated many times. */ + err = acquire_rng(random_bytes, sizeof(random_bytes)); + if (err != EFI_SUCCESS) { + size = 0; + /* If we can't get any randomness from EFI itself, then we'll only be relying on what's in + * ESP. But ESP is mutable, so if secure boot is enabled, we probably shouldn't trust that + * alone, in which case we bail out early. */ + if (!seeded_by_efi && secure_boot_enabled()) + return EFI_NOT_FOUND; + } else { + seeded_by_efi = true; + size = sizeof(random_bytes); + } + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(random_bytes, size, &hash); + + /* Get some system specific seed that the installer might have placed in an EFI variable. We include + * it in our hash. This is protection against golden master image sloppiness, and it remains on the + * system, even when disk images are duplicated or swapped out. */ + size = 0; + err = acquire_system_token(&system_token, &size); + if ((err != EFI_SUCCESS || size < DESIRED_SEED_SIZE) && !seeded_by_efi) + return err; + sha256_process_bytes(&size, sizeof(size), &hash); + if (system_token) { + sha256_process_bytes(system_token, size, &hash); + explicit_bzero_safe(system_token, size); + } + + err = root_dir->Open( + root_dir, + &handle, + (char16_t *) u"\\loader\\random-seed", + EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, + 0); + if (err != EFI_SUCCESS) { + if (err != EFI_NOT_FOUND && err != EFI_WRITE_PROTECTED) + log_error_status(err, "Failed to open random seed file: %m"); + return err; + } + + err = get_file_info(handle, &info, NULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to get file info for random seed: %m"); + + size = info->FileSize; + if (size < RANDOM_MAX_SIZE_MIN) + return log_error("Random seed file is too short."); + + if (size > RANDOM_MAX_SIZE_MAX) + return log_error("Random seed file is too large."); + + seed = xmalloc(size); + rsize = size; + err = handle->Read(handle, &rsize, seed); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read random seed file: %m"); + if (rsize != size) { + explicit_bzero_safe(seed, rsize); + return log_error_status(EFI_PROTOCOL_ERROR, "Short read on random seed file."); + } + + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(seed, size, &hash); + explicit_bzero_safe(seed, size); + + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + + /* Let's also include the UEFI monotonic counter (which is supposedly increasing on every single + * boot) in the hash, so that even if the changes to the ESP for some reason should not be + * persistent, the random seed we generate will still be different on every single boot. */ + err = BS->GetNextMonotonicCount(&uefi_monotonic_counter); + if (err != EFI_SUCCESS && !seeded_by_efi) + return log_error_status(err, "Failed to acquire UEFI monotonic counter: %m"); + size = sizeof(uefi_monotonic_counter); + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&uefi_monotonic_counter, size, &hash); + + err = RT->GetTime(&now, NULL); + size = err == EFI_SUCCESS ? sizeof(now) : 0; /* Known to be flaky, so don't bark on error. */ + sha256_process_bytes(&size, sizeof(size), &hash); + sha256_process_bytes(&now, size, &hash); + + /* hash_key = HASH(hash) */ + sha256_finish_ctx(&hash, hash_key); + + /* hash = hash_key || 0 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 0 }, sizeof(uint8_t), &hash); + /* random_bytes = HASH(hash) */ + sha256_finish_ctx(&hash, random_bytes); + + size = sizeof(random_bytes); + /* If the file size is too large, zero out the remaining bytes on disk. */ + if (size < info->FileSize) { + err = handle->SetPosition(handle, size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to offset of random seed file: %m"); + wsize = info->FileSize - size; + err = handle->Write(handle, &wsize, seed /* All zeros now */); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to write random seed file: %m"); + if (wsize != info->FileSize - size) + return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file."); + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to flush random seed file: %m"); + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to seek to beginning of random seed file: %m"); + + /* We could truncate the file here with something like: + * + * info->FileSize = size; + * err = handle->SetInfo(handle, &GenericFileInfo, info->Size, info); + * if (err != EFI_SUCCESS) + * return log_error_status(err, "Failed to truncate random seed file: %u"); + * + * But this is considered slightly risky, because EFI filesystem drivers are a little bit + * flimsy. So instead we rely on userspace eventually truncating this when it writes a new + * seed. For now the best we do is zero it. */ + } + /* Update the random seed on disk before we use it */ + wsize = size; + err = handle->Write(handle, &wsize, random_bytes); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to write random seed file: %m"); + if (wsize != size) + return log_error_status(EFI_PROTOCOL_ERROR, "Short write on random seed file."); + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to flush random seed file: %m"); + + err = BS->AllocatePool(EfiACPIReclaimMemory, + offsetof(struct linux_efi_random_seed, seed) + DESIRED_SEED_SIZE, + (void **) &new_seed_table); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to allocate EFI table for random seed: %m"); + new_seed_table->size = DESIRED_SEED_SIZE; + + /* hash = hash_key || 1 */ + sha256_init_ctx(&hash); + sha256_process_bytes(hash_key, sizeof(hash_key), &hash); + sha256_process_bytes(&(const uint8_t){ 1 }, sizeof(uint8_t), &hash); + /* new_seed_table->seed = HASH(hash) */ + sha256_finish_ctx(&hash, new_seed_table->seed); + + err = BS->InstallConfigurationTable(MAKE_GUID_PTR(LINUX_EFI_RANDOM_SEED_TABLE), new_seed_table); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to install EFI table for random seed: %m"); + TAKE_PTR(new_seed_table); + + if (previous_seed_table) { + /* Now that we've succeeded in installing the new table, we can safely nuke the old one. */ + explicit_bzero_safe(previous_seed_table->seed, previous_seed_table->size); + explicit_bzero_safe(previous_seed_table, sizeof(*previous_seed_table)); + free(previous_seed_table); + } + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/random-seed.h b/src/boot/efi/random-seed.h new file mode 100644 index 0000000..67f005d --- /dev/null +++ b/src/boot/efi/random-seed.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS process_random_seed(EFI_FILE *root_dir); diff --git a/src/boot/efi/secure-boot.c b/src/boot/efi/secure-boot.c new file mode 100644 index 0000000..f76d2f4 --- /dev/null +++ b/src/boot/efi/secure-boot.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "console.h" +#include "proto/security-arch.h" +#include "secure-boot.h" +#include "util.h" +#include "vmm.h" + +bool secure_boot_enabled(void) { + bool secure = false; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + err = efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"SecureBoot", &secure); + + return err == EFI_SUCCESS && secure; +} + +SecureBootMode secure_boot_mode(void) { + bool secure, audit = false, deployed = false, setup = false; + EFI_STATUS err; + + err = efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"SecureBoot", &secure); + if (err != EFI_SUCCESS) + return SECURE_BOOT_UNSUPPORTED; + + /* We can assume false for all these if they are abscent (AuditMode and + * DeployedMode may not exist on older firmware). */ + (void) efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"AuditMode", &audit); + (void) efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"DeployedMode", &deployed); + (void) efivar_get_boolean_u8(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"SetupMode", &setup); + + return decode_secure_boot_mode(secure, audit, deployed, setup); +} + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path, bool force) { + assert(root_dir); + assert(path); + + EFI_STATUS err; + + clear_screen(COLOR_NORMAL); + + /* Enrolling secure boot keys is safe to do in virtualized environments as there is nothing + * we can brick there. */ + bool is_safe = in_hypervisor(); + + if (!is_safe && !force) + return EFI_SUCCESS; + + printf("Enrolling secure boot keys from directory: %ls\n", path); + + if (!is_safe) { + printf("Warning: Enrolling custom Secure Boot keys might soft-brick your machine!\n"); + + unsigned timeout_sec = 15; + for (;;) { + printf("\rEnrolling in %2u s, press any key to abort.", timeout_sec); + + uint64_t key; + err = console_key_read(&key, 1000 * 1000); + if (err == EFI_NOT_READY) + continue; + if (err == EFI_TIMEOUT) { + if (timeout_sec == 0) /* continue enrolling keys */ + break; + timeout_sec--; + continue; + } + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Error waiting for user input to enroll Secure Boot keys: %m"); + + /* user aborted, returning EFI_SUCCESS here allows the user to go back to the menu */ + return EFI_SUCCESS; + } + + printf("\n"); + } + + _cleanup_(file_closep) EFI_FILE *dir = NULL; + + err = open_directory(root_dir, path, &dir); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed opening keys directory %ls: %m", path); + + struct { + const char16_t *name; + const char16_t *filename; + const EFI_GUID vendor; + char *buffer; + size_t size; + } sb_vars[] = { + { u"db", u"db.auth", EFI_IMAGE_SECURITY_DATABASE_GUID, NULL, 0 }, + { u"KEK", u"KEK.auth", EFI_GLOBAL_VARIABLE, NULL, 0 }, + { u"PK", u"PK.auth", EFI_GLOBAL_VARIABLE, NULL, 0 }, + }; + + /* Make sure all keys files exist before we start enrolling them by loading them from the disk first. */ + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) { + err = file_read(dir, sb_vars[i].filename, 0, 0, &sb_vars[i].buffer, &sb_vars[i].size); + if (err != EFI_SUCCESS) { + log_error_status(err, "Failed reading file %ls\\%ls: %m", path, sb_vars[i].filename); + goto out_deallocate; + } + } + + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) { + uint32_t sb_vars_opts = + EFI_VARIABLE_NON_VOLATILE | + EFI_VARIABLE_BOOTSERVICE_ACCESS | + EFI_VARIABLE_RUNTIME_ACCESS | + EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS; + + err = efivar_set_raw(&sb_vars[i].vendor, sb_vars[i].name, sb_vars[i].buffer, sb_vars[i].size, sb_vars_opts); + if (err != EFI_SUCCESS) { + log_error_status(err, "Failed to write %ls secure boot variable: %m", sb_vars[i].name); + goto out_deallocate; + } + } + + printf("Custom Secure Boot keys successfully enrolled, rebooting the system now!\n"); + /* The system should be in secure boot mode now and we could continue a regular boot. But at least + * TPM PCR7 measurements should change on next boot. Reboot now so that any OS we load does not end + * up relying on the old PCR state. */ + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); + +out_deallocate: + for (size_t i = 0; i < ELEMENTSOF(sb_vars); i++) + free(sb_vars[i].buffer); + + return err; +} + +static struct SecurityOverride { + EFI_SECURITY_ARCH_PROTOCOL *security; + EFI_SECURITY2_ARCH_PROTOCOL *security2; + EFI_SECURITY_FILE_AUTHENTICATION_STATE original_hook; + EFI_SECURITY2_FILE_AUTHENTICATION original_hook2; + + security_validator_t validator; + const void *validator_ctx; +} security_override; + +static EFIAPI EFI_STATUS security_hook( + const EFI_SECURITY_ARCH_PROTOCOL *this, + uint32_t authentication_status, + const EFI_DEVICE_PATH *file) { + + assert(security_override.validator); + assert(security_override.security); + assert(security_override.original_hook); + + if (security_override.validator(security_override.validator_ctx, file, NULL, 0)) + return EFI_SUCCESS; + + return security_override.original_hook(security_override.security, authentication_status, file); +} + +static EFIAPI EFI_STATUS security2_hook( + const EFI_SECURITY2_ARCH_PROTOCOL *this, + const EFI_DEVICE_PATH *device_path, + void *file_buffer, + size_t file_size, + bool boot_policy) { + + assert(security_override.validator); + assert(security_override.security2); + assert(security_override.original_hook2); + + if (security_override.validator(security_override.validator_ctx, device_path, file_buffer, file_size)) + return EFI_SUCCESS; + + return security_override.original_hook2( + security_override.security2, device_path, file_buffer, file_size, boot_policy); +} + +/* This replaces the platform provided security arch protocols hooks (defined in the UEFI Platform + * Initialization Specification) with our own that uses the given validator to decide if a image is to be + * trusted. If not running in secure boot or the protocols are not available nothing happens. The override + * must be removed with uninstall_security_override() after LoadImage() has been called. + * + * This is a hack as we do not own the security protocol instances and modifying them is not an official part + * of their spec. But there is little else we can do to circumvent secure boot short of implementing our own + * PE loader. We could replace the firmware instances with our own instance using + * ReinstallProtocolInterface(), but some firmware will still use the old ones. */ +void install_security_override(security_validator_t validator, const void *validator_ctx) { + EFI_STATUS err; + + assert(validator); + + if (!secure_boot_enabled()) + return; + + security_override = (struct SecurityOverride) { + .validator = validator, + .validator_ctx = validator_ctx, + }; + + EFI_SECURITY_ARCH_PROTOCOL *security = NULL; + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_SECURITY_ARCH_PROTOCOL), NULL, (void **) &security); + if (err == EFI_SUCCESS) { + security_override.security = security; + security_override.original_hook = security->FileAuthenticationState; + security->FileAuthenticationState = security_hook; + } + + EFI_SECURITY2_ARCH_PROTOCOL *security2 = NULL; + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_SECURITY2_ARCH_PROTOCOL), NULL, (void **) &security2); + if (err == EFI_SUCCESS) { + security_override.security2 = security2; + security_override.original_hook2 = security2->FileAuthentication; + security2->FileAuthentication = security2_hook; + } +} + +void uninstall_security_override(void) { + if (security_override.original_hook) + security_override.security->FileAuthenticationState = security_override.original_hook; + if (security_override.original_hook2) + security_override.security2->FileAuthentication = security_override.original_hook2; +} diff --git a/src/boot/efi/secure-boot.h b/src/boot/efi/secure-boot.h new file mode 100644 index 0000000..3471131 --- /dev/null +++ b/src/boot/efi/secure-boot.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "efivars-fundamental.h" + +typedef enum { + ENROLL_OFF, /* no Secure Boot key enrollment whatsoever, even manual entries are not generated */ + ENROLL_MANUAL, /* Secure Boot key enrollment is strictly manual: manual entries are generated and need to be selected by the user */ + ENROLL_IF_SAFE, /* Automatically enroll if it is safe (if we are running inside a VM, for example). */ + ENROLL_FORCE, /* Secure Boot key enrollment may be automatic if it is available but might not be safe */ +} secure_boot_enroll; + +bool secure_boot_enabled(void); +SecureBootMode secure_boot_mode(void); + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path, bool force); + +typedef bool (*security_validator_t)( + const void *ctx, + const EFI_DEVICE_PATH *device_path, + const void *file_buffer, + size_t file_size); + +void install_security_override(security_validator_t validator, const void *validator_ctx); +void uninstall_security_override(void); diff --git a/src/boot/efi/shim.c b/src/boot/efi/shim.c new file mode 100644 index 0000000..df136ed --- /dev/null +++ b/src/boot/efi/shim.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Port to systemd-boot + * Copyright © 2017 Max Resch + * + * Security Policy Handling + * Copyright © 2012 + * https://github.com/mjg59/efitools + */ + +#include "device-path-util.h" +#include "secure-boot.h" +#include "shim.h" +#include "util.h" + +#if defined(__x86_64__) || defined(__i386__) +#define __sysv_abi__ __attribute__((sysv_abi)) +#else +#define __sysv_abi__ +#endif + +struct ShimLock { + EFI_STATUS __sysv_abi__ (*shim_verify) (const void *buffer, uint32_t size); + + /* context is actually a struct for the PE header, but it isn't needed so void is sufficient just do define the interface + * see shim.c/shim.h and PeHeader.h in the github shim repo */ + EFI_STATUS __sysv_abi__ (*generate_hash) (void *data, uint32_t datasize, void *context, uint8_t *sha256hash, uint8_t *sha1hash); + + EFI_STATUS __sysv_abi__ (*read_header) (void *data, uint32_t datasize, void *context); +}; + +#define SHIM_LOCK_GUID \ + { 0x605dab50, 0xe046, 0x4300, { 0xab, 0xb6, 0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23 } } + +bool shim_loaded(void) { + struct ShimLock *shim_lock; + + return BS->LocateProtocol(MAKE_GUID_PTR(SHIM_LOCK), NULL, (void **) &shim_lock) == EFI_SUCCESS; +} + +static bool shim_validate( + const void *ctx, const EFI_DEVICE_PATH *device_path, const void *file_buffer, size_t file_size) { + + EFI_STATUS err; + _cleanup_free_ char *file_buffer_owned = NULL; + + if (!file_buffer) { + if (!device_path) + return false; + + EFI_HANDLE device_handle; + EFI_DEVICE_PATH *file_dp = (EFI_DEVICE_PATH *) device_path; + err = BS->LocateDevicePath( + MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), &file_dp, &device_handle); + if (err != EFI_SUCCESS) + return false; + + _cleanup_(file_closep) EFI_FILE *root = NULL; + err = open_volume(device_handle, &root); + if (err != EFI_SUCCESS) + return false; + + _cleanup_free_ char16_t *dp_str = NULL; + err = device_path_to_str(file_dp, &dp_str); + if (err != EFI_SUCCESS) + return false; + + err = file_read(root, dp_str, 0, 0, &file_buffer_owned, &file_size); + if (err != EFI_SUCCESS) + return false; + + file_buffer = file_buffer_owned; + } + + struct ShimLock *shim_lock; + err = BS->LocateProtocol(MAKE_GUID_PTR(SHIM_LOCK), NULL, (void **) &shim_lock); + if (err != EFI_SUCCESS) + return false; + + return shim_lock->shim_verify(file_buffer, file_size) == EFI_SUCCESS; +} + +EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image) { + assert(device_path); + assert(ret_image); + + bool have_shim = shim_loaded(); + + if (have_shim) + install_security_override(shim_validate, NULL); + + EFI_STATUS ret = BS->LoadImage( + /*BootPolicy=*/false, parent, (EFI_DEVICE_PATH *) device_path, NULL, 0, ret_image); + + if (have_shim) + uninstall_security_override(); + + return ret; +} + +void shim_retain_protocol(void) { + uint8_t value = 1; + + /* Ask Shim to avoid uninstalling its security protocol, so that we can use it from sd-stub to + * validate PE addons. By default, Shim uninstalls its protocol when calling StartImage(). + * Requires Shim 15.8. */ + (void) efivar_set_raw(MAKE_GUID_PTR(SHIM_LOCK), u"ShimRetainProtocol", &value, sizeof(value), 0); +} diff --git a/src/boot/efi/shim.h b/src/boot/efi/shim.h new file mode 100644 index 0000000..e0cb39f --- /dev/null +++ b/src/boot/efi/shim.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Port to systemd-boot + * Copyright © 2017 Max Resch + * + * Security Policy Handling + * Copyright © 2012 + * https://github.com/mjg59/efitools + */ +#pragma once + +#include "efi.h" + +bool shim_loaded(void); +EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image); +void shim_retain_protocol(void); diff --git a/src/boot/efi/splash.c b/src/boot/efi/splash.c new file mode 100644 index 0000000..8daeb71 --- /dev/null +++ b/src/boot/efi/splash.c @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "graphics.h" +#include "logarithm.h" +#include "proto/graphics-output.h" +#include "splash.h" +#include "unaligned-fundamental.h" +#include "util.h" + +struct bmp_file { + char signature[2]; + uint32_t size; + uint16_t reserved[2]; + uint32_t offset; +} _packed_; + +/* we require at least BITMAPINFOHEADER, later versions are + accepted, but their features ignored */ +struct bmp_dib { + uint32_t size; + uint32_t x; + uint32_t y; + uint16_t planes; + uint16_t depth; + uint32_t compression; + uint32_t image_size; + int32_t x_pixel_meter; + int32_t y_pixel_meter; + uint32_t colors_used; + uint32_t colors_important; + uint32_t channel_mask_r; + uint32_t channel_mask_g; + uint32_t channel_mask_b; + uint32_t channel_mask_a; +} _packed_; + +#define SIZEOF_BMP_DIB offsetof(struct bmp_dib, channel_mask_r) +#define SIZEOF_BMP_DIB_RGB offsetof(struct bmp_dib, channel_mask_a) +#define SIZEOF_BMP_DIB_RGBA sizeof(struct bmp_dib) + +struct bmp_map { + uint8_t blue; + uint8_t green; + uint8_t red; + uint8_t reserved; +} _packed_; + +static EFI_STATUS bmp_parse_header( + const uint8_t *bmp, + size_t size, + struct bmp_dib **ret_dib, + struct bmp_map **ret_map, + const uint8_t **pixmap) { + + assert(bmp); + assert(ret_dib); + assert(ret_map); + assert(pixmap); + + if (size < sizeof(struct bmp_file) + SIZEOF_BMP_DIB) + return EFI_INVALID_PARAMETER; + + /* check file header */ + struct bmp_file *file = (struct bmp_file *) bmp; + if (file->signature[0] != 'B' || file->signature[1] != 'M') + return EFI_INVALID_PARAMETER; + if (file->size != size) + return EFI_INVALID_PARAMETER; + if (file->size < file->offset) + return EFI_INVALID_PARAMETER; + + /* check device-independent bitmap */ + struct bmp_dib *dib = (struct bmp_dib *) (bmp + sizeof(struct bmp_file)); + if (dib->size < SIZEOF_BMP_DIB) + return EFI_UNSUPPORTED; + + switch (dib->depth) { + case 1: + case 4: + case 8: + case 24: + if (dib->compression != 0) + return EFI_UNSUPPORTED; + + break; + + case 16: + case 32: + if (dib->compression != 0 && dib->compression != 3) + return EFI_UNSUPPORTED; + + break; + + default: + return EFI_UNSUPPORTED; + } + + size_t row_size = ((size_t) dib->depth * dib->x + 31) / 32 * 4; + if (file->size - file->offset < dib->y * row_size) + return EFI_INVALID_PARAMETER; + if (row_size * dib->y > 64 * 1024 * 1024) + return EFI_INVALID_PARAMETER; + + /* check color table */ + struct bmp_map *map = (struct bmp_map *) (bmp + sizeof(struct bmp_file) + dib->size); + if (file->offset < sizeof(struct bmp_file) + dib->size) + return EFI_INVALID_PARAMETER; + + if (file->offset > sizeof(struct bmp_file) + dib->size) { + uint32_t map_count = 0; + + if (dib->colors_used) + map_count = dib->colors_used; + else if (IN_SET(dib->depth, 1, 4, 8)) + map_count = 1 << dib->depth; + + size_t map_size = file->offset - (sizeof(struct bmp_file) + dib->size); + if (map_size != sizeof(struct bmp_map) * map_count) + return EFI_INVALID_PARAMETER; + } + + *ret_map = map; + *ret_dib = dib; + *pixmap = bmp + file->offset; + + return EFI_SUCCESS; +} + +enum Channels { R, G, B, A, _CHANNELS_MAX }; +static void read_channel_maks( + const struct bmp_dib *dib, + uint32_t channel_mask[static _CHANNELS_MAX], + uint8_t channel_shift[static _CHANNELS_MAX], + uint8_t channel_scale[static _CHANNELS_MAX]) { + + assert(dib); + + if (IN_SET(dib->depth, 16, 32) && dib->size >= SIZEOF_BMP_DIB_RGB) { + channel_mask[R] = dib->channel_mask_r; + channel_mask[G] = dib->channel_mask_g; + channel_mask[B] = dib->channel_mask_b; + channel_shift[R] = __builtin_ctz(dib->channel_mask_r); + channel_shift[G] = __builtin_ctz(dib->channel_mask_g); + channel_shift[B] = __builtin_ctz(dib->channel_mask_b); + channel_scale[R] = 0xff / ((1 << popcount(dib->channel_mask_r)) - 1); + channel_scale[G] = 0xff / ((1 << popcount(dib->channel_mask_g)) - 1); + channel_scale[B] = 0xff / ((1 << popcount(dib->channel_mask_b)) - 1); + + if (dib->size >= SIZEOF_BMP_DIB_RGBA && dib->channel_mask_a != 0) { + channel_mask[A] = dib->channel_mask_a; + channel_shift[A] = __builtin_ctz(dib->channel_mask_a); + channel_scale[A] = 0xff / ((1 << popcount(dib->channel_mask_a)) - 1); + } else { + channel_mask[A] = 0; + channel_shift[A] = 0; + channel_scale[A] = 0; + } + } else { + bool bpp16 = dib->depth == 16; + channel_mask[R] = bpp16 ? 0x7C00 : 0xFF0000; + channel_mask[G] = bpp16 ? 0x03E0 : 0x00FF00; + channel_mask[B] = bpp16 ? 0x001F : 0x0000FF; + channel_mask[A] = bpp16 ? 0x0000 : 0x000000; + channel_shift[R] = bpp16 ? 0xA : 0x10; + channel_shift[G] = bpp16 ? 0x5 : 0x08; + channel_shift[B] = bpp16 ? 0x0 : 0x00; + channel_shift[A] = bpp16 ? 0x0 : 0x00; + channel_scale[R] = bpp16 ? 0x08 : 0x1; + channel_scale[G] = bpp16 ? 0x08 : 0x1; + channel_scale[B] = bpp16 ? 0x08 : 0x1; + channel_scale[A] = bpp16 ? 0x00 : 0x0; + } +} + +static EFI_STATUS bmp_to_blt( + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *buf, + struct bmp_dib *dib, + struct bmp_map *map, + const uint8_t *pixmap) { + + const uint8_t *in; + + assert(buf); + assert(dib); + assert(map); + assert(pixmap); + + uint32_t channel_mask[_CHANNELS_MAX]; + uint8_t channel_shift[_CHANNELS_MAX], channel_scale[_CHANNELS_MAX]; + read_channel_maks(dib, channel_mask, channel_shift, channel_scale); + + /* transform and copy pixels */ + in = pixmap; + for (uint32_t y = 0; y < dib->y; y++) { + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *out = &buf[(dib->y - y - 1) * dib->x]; + + for (uint32_t x = 0; x < dib->x; x++, in++, out++) { + switch (dib->depth) { + case 1: { + for (unsigned i = 0; i < 8 && x < dib->x; i++) { + out->Red = map[((*in) >> (7 - i)) & 1].red; + out->Green = map[((*in) >> (7 - i)) & 1].green; + out->Blue = map[((*in) >> (7 - i)) & 1].blue; + out++; + x++; + } + out--; + x--; + break; + } + + case 4: { + unsigned i = (*in) >> 4; + out->Red = map[i].red; + out->Green = map[i].green; + out->Blue = map[i].blue; + if (x < (dib->x - 1)) { + out++; + x++; + i = (*in) & 0x0f; + out->Red = map[i].red; + out->Green = map[i].green; + out->Blue = map[i].blue; + } + break; + } + + case 8: + out->Red = map[*in].red; + out->Green = map[*in].green; + out->Blue = map[*in].blue; + break; + + case 24: + out->Red = in[2]; + out->Green = in[1]; + out->Blue = in[0]; + in += 2; + break; + + case 16: + case 32: { + uint32_t i = dib->depth == 16 ? unaligned_read_ne16(in) : + unaligned_read_ne32(in); + + uint8_t r = ((i & channel_mask[R]) >> channel_shift[R]) * channel_scale[R], + g = ((i & channel_mask[G]) >> channel_shift[G]) * channel_scale[G], + b = ((i & channel_mask[B]) >> channel_shift[B]) * channel_scale[B], + a = 0xFFu; + if (channel_mask[A] != 0) + a = ((i & channel_mask[A]) >> channel_shift[A]) * channel_scale[A]; + + out->Red = (out->Red * (0xFFu - a) + r * a) >> 8; + out->Green = (out->Green * (0xFFu - a) + g * a) >> 8; + out->Blue = (out->Blue * (0xFFu - a) + b * a) >> 8; + + in += dib->depth == 16 ? 1 : 3; + break; + } + } + } + + /* add row padding; new lines always start at 32 bit boundary */ + size_t row_size = in - pixmap; + in += ((row_size + 3) & ~3) - row_size; + } + + return EFI_SUCCESS; +} + +EFI_STATUS graphics_splash(const uint8_t *content, size_t len) { + EFI_GRAPHICS_OUTPUT_BLT_PIXEL background = {}; + EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL; + struct bmp_dib *dib; + struct bmp_map *map; + const uint8_t *pixmap; + size_t x_pos = 0, y_pos = 0; + EFI_STATUS err; + + if (len == 0) + return EFI_SUCCESS; + + assert(content); + + if (strcaseeq16(ST->FirmwareVendor, u"Apple")) { + background.Red = 0xc0; + background.Green = 0xc0; + background.Blue = 0xc0; + } + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), NULL, (void **) &GraphicsOutput); + if (err != EFI_SUCCESS) + return err; + + err = bmp_parse_header(content, len, &dib, &map, &pixmap); + if (err != EFI_SUCCESS) + return err; + + if (dib->x < GraphicsOutput->Mode->Info->HorizontalResolution) + x_pos = (GraphicsOutput->Mode->Info->HorizontalResolution - dib->x) / 2; + if (dib->y < GraphicsOutput->Mode->Info->VerticalResolution) + y_pos = (GraphicsOutput->Mode->Info->VerticalResolution - dib->y) / 2; + + err = GraphicsOutput->Blt( + GraphicsOutput, &background, + EfiBltVideoFill, 0, 0, 0, 0, + GraphicsOutput->Mode->Info->HorizontalResolution, + GraphicsOutput->Mode->Info->VerticalResolution, 0); + if (err != EFI_SUCCESS) + return err; + + /* Read in current screen content to perform proper alpha blending. */ + _cleanup_free_ EFI_GRAPHICS_OUTPUT_BLT_PIXEL *blt = xnew( + EFI_GRAPHICS_OUTPUT_BLT_PIXEL, dib->x * dib->y); + err = GraphicsOutput->Blt( + GraphicsOutput, blt, + EfiBltVideoToBltBuffer, x_pos, y_pos, 0, 0, + dib->x, dib->y, 0); + if (err != EFI_SUCCESS) + return err; + + err = bmp_to_blt(blt, dib, map, pixmap); + if (err != EFI_SUCCESS) + return err; + + err = graphics_mode(true); + if (err != EFI_SUCCESS) + return err; + + return GraphicsOutput->Blt( + GraphicsOutput, blt, + EfiBltBufferToVideo, 0, 0, x_pos, y_pos, + dib->x, dib->y, 0); +} diff --git a/src/boot/efi/splash.h b/src/boot/efi/splash.h new file mode 100644 index 0000000..a66eb24 --- /dev/null +++ b/src/boot/efi/splash.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS graphics_splash(const uint8_t *content, size_t len); diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c new file mode 100644 index 0000000..7ef3e76 --- /dev/null +++ b/src/boot/efi/stub.c @@ -0,0 +1,816 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "device-path-util.h" +#include "devicetree.h" +#include "graphics.h" +#include "linux.h" +#include "measure.h" +#include "memory-util-fundamental.h" +#include "part-discovery.h" +#include "pe.h" +#include "proto/shell-parameters.h" +#include "random-seed.h" +#include "sbat.h" +#include "secure-boot.h" +#include "shim.h" +#include "splash.h" +#include "tpm2-pcr.h" +#include "uki.h" +#include "util.h" +#include "version.h" +#include "vmm.h" + +/* magic string to find in the binary image */ +DECLARE_NOALLOC_SECTION(".sdmagic", "#### LoaderInfo: systemd-stub " GIT_VERSION " ####"); + +DECLARE_SBAT(SBAT_STUB_SECTION_TEXT); + +static EFI_STATUS combine_initrd( + EFI_PHYSICAL_ADDRESS initrd_base, size_t initrd_size, + const void * const extra_initrds[], const size_t extra_initrd_sizes[], size_t n_extra_initrds, + Pages *ret_initr_pages, size_t *ret_initrd_size) { + + size_t n; + + assert(ret_initr_pages); + assert(ret_initrd_size); + + /* Combines four initrds into one, by simple concatenation in memory */ + + n = ALIGN4(initrd_size); /* main initrd might not be padded yet */ + + for (size_t i = 0; i < n_extra_initrds; i++) { + if (!extra_initrds[i]) + continue; + + if (n > SIZE_MAX - extra_initrd_sizes[i]) + return EFI_OUT_OF_RESOURCES; + + n += extra_initrd_sizes[i]; + } + + _cleanup_pages_ Pages pages = xmalloc_pages( + AllocateMaxAddress, + EfiLoaderData, + EFI_SIZE_TO_PAGES(n), + UINT32_MAX /* Below 4G boundary. */); + uint8_t *p = PHYSICAL_ADDRESS_TO_POINTER(pages.addr); + if (initrd_base != 0) { + size_t pad; + + /* Order matters, the real initrd must come first, since it might include microcode updates + * which the kernel only looks for in the first cpio archive */ + p = mempcpy(p, PHYSICAL_ADDRESS_TO_POINTER(initrd_base), initrd_size); + + pad = ALIGN4(initrd_size) - initrd_size; + if (pad > 0) { + memzero(p, pad); + p += pad; + } + } + + for (size_t i = 0; i < n_extra_initrds; i++) { + if (!extra_initrds[i]) + continue; + + p = mempcpy(p, extra_initrds[i], extra_initrd_sizes[i]); + } + + assert(PHYSICAL_ADDRESS_TO_POINTER(pages.addr + n) == p); + + *ret_initr_pages = pages; + *ret_initrd_size = n; + pages.n_pages = 0; + + return EFI_SUCCESS; +} + +static void export_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { + static const uint64_t stub_features = + EFI_STUB_FEATURE_REPORT_BOOT_PARTITION | /* We set LoaderDevicePartUUID */ + EFI_STUB_FEATURE_PICK_UP_CREDENTIALS | /* We pick up credentials from the boot partition */ + EFI_STUB_FEATURE_PICK_UP_SYSEXTS | /* We pick up system extensions from the boot partition */ + EFI_STUB_FEATURE_THREE_PCRS | /* We can measure kernel image, parameters and sysext */ + EFI_STUB_FEATURE_RANDOM_SEED | /* We pass a random seed to the kernel */ + EFI_STUB_FEATURE_CMDLINE_ADDONS | /* We pick up .cmdline addons */ + EFI_STUB_FEATURE_CMDLINE_SMBIOS | /* We support extending kernel cmdline from SMBIOS Type #11 */ + EFI_STUB_FEATURE_DEVICETREE_ADDONS | /* We pick up .dtb addons */ + 0; + + assert(loaded_image); + + /* Export the device path this image is started from, if it's not set yet */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); + if (uuid) + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", uuid, 0); + } + + /* If LoaderImageIdentifier is not set, assume the image with this stub was loaded directly from the + * UEFI firmware without any boot loader, and hence set the LoaderImageIdentifier ourselves. Note + * that some boot chain loaders neither set LoaderImageIdentifier nor make FilePath available to us, + * in which case there's simple nothing to set for us. (The UEFI spec doesn't really say who's wrong + * here, i.e. whether FilePath may be NULL or not, hence handle this gracefully and check if FilePath + * is non-NULL explicitly.) */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderImageIdentifier", NULL, NULL) != EFI_SUCCESS && + loaded_image->FilePath) { + _cleanup_free_ char16_t *s = NULL; + if (device_path_to_str(loaded_image->FilePath, &s) == EFI_SUCCESS) + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderImageIdentifier", s, 0); + } + + /* if LoaderFirmwareInfo is not set, let's set it */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xasprintf("%ls %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareInfo", s, 0); + } + + /* ditto for LoaderFirmwareType */ + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareType", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xasprintf("UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(MAKE_GUID_PTR(LOADER), u"LoaderFirmwareType", s, 0); + } + + + /* add StubInfo (this is one is owned by the stub, hence we unconditionally override this with our + * own data) */ + (void) efivar_set(MAKE_GUID_PTR(LOADER), u"StubInfo", u"systemd-stub " GIT_VERSION, 0); + + (void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"StubFeatures", stub_features, 0); +} + +static bool use_load_options( + EFI_HANDLE stub_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + bool have_cmdline, + char16_t **ret) { + + assert(stub_image); + assert(loaded_image); + assert(ret); + + /* We only allow custom command lines if we aren't in secure boot or if no cmdline was baked into + * the stub image. + * We also don't allow it if we are in confidential vms and secureboot is on. */ + if (secure_boot_enabled() && (have_cmdline || is_confidential_vm())) + return false; + + /* We also do a superficial check whether first character of passed command line + * is printable character (for compat with some Dell systems which fill in garbage?). */ + if (loaded_image->LoadOptionsSize < sizeof(char16_t) || ((char16_t *) loaded_image->LoadOptions)[0] <= 0x1F) + return false; + + /* The UEFI shell registers EFI_SHELL_PARAMETERS_PROTOCOL onto images it runs. This lets us know that + * LoadOptions starts with the stub binary path which we want to strip off. */ + EFI_SHELL_PARAMETERS_PROTOCOL *shell; + if (BS->HandleProtocol(stub_image, MAKE_GUID_PTR(EFI_SHELL_PARAMETERS_PROTOCOL), (void **) &shell) + != EFI_SUCCESS) { + /* Not running from EFI shell, use entire LoadOptions. Note that LoadOptions is a void*, so + * it could be anything! */ + *ret = xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t)); + mangle_stub_cmdline(*ret); + return true; + } + + if (shell->Argc < 2) + /* No arguments were provided? Then we fall back to built-in cmdline. */ + return false; + + /* Assemble the command line ourselves without our stub path. */ + *ret = xstrdup16(shell->Argv[1]); + for (size_t i = 2; i < shell->Argc; i++) { + _cleanup_free_ char16_t *old = *ret; + *ret = xasprintf("%ls %ls", old, shell->Argv[i]); + } + + mangle_stub_cmdline(*ret); + return true; +} + +static EFI_STATUS load_addons_from_dir( + EFI_FILE *root, + const char16_t *prefix, + char16_t ***items, + size_t *n_items, + size_t *n_allocated) { + + _cleanup_(file_closep) EFI_FILE *extra_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + size_t dirent_size = 0; + EFI_STATUS err; + + assert(root); + assert(prefix); + assert(items); + assert(n_items); + assert(n_allocated); + + err = open_directory(root, prefix, &extra_dir); + if (err == EFI_NOT_FOUND) + /* No extra subdir, that's totally OK */ + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open addons directory '%ls': %m", prefix); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read addons directory of loaded image: %m"); + if (!dirent) /* End of directory */ + break; + + if (dirent->FileName[0] == '.') + continue; + if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (!is_ascii(dirent->FileName)) + continue; + if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */ + continue; + if (!endswith_no_case(dirent->FileName, u".addon.efi")) + continue; + + d = xstrdup16(dirent->FileName); + + if (*n_items + 2 > *n_allocated) { + /* We allocate 16 entries at a time, as a matter of optimization */ + if (*n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + size_t m = *n_items + 16; + *items = xrealloc(*items, *n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *)); + *n_allocated = m; + } + + (*items)[(*n_items)++] = TAKE_PTR(d); + (*items)[*n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */ + } + + return EFI_SUCCESS; +} + +static void cmdline_append_and_measure_addons( + char16_t *cmdline_global, + char16_t *cmdline_uki, + char16_t **cmdline_append, + bool *ret_parameters_measured) { + + _cleanup_free_ char16_t *tmp = NULL, *merged = NULL; + bool m = false; + + assert(cmdline_append); + assert(ret_parameters_measured); + + if (isempty(cmdline_global) && isempty(cmdline_uki)) + return; + + merged = xasprintf("%ls%ls%ls", + strempty(cmdline_global), + isempty(cmdline_global) || isempty(cmdline_uki) ? u"" : u" ", + strempty(cmdline_uki)); + + mangle_stub_cmdline(merged); + + if (isempty(merged)) + return; + + (void) tpm_log_load_options(merged, &m); + *ret_parameters_measured = m; + + tmp = TAKE_PTR(*cmdline_append); + *cmdline_append = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", merged); +} + +static void dtb_install_addons( + struct devicetree_state *dt_state, + void **dt_bases, + size_t *dt_sizes, + char16_t **dt_filenames, + size_t n_dts, + bool *ret_parameters_measured) { + + int parameters_measured = -1; + EFI_STATUS err; + + assert(dt_state); + assert(n_dts == 0 || (dt_bases && dt_sizes && dt_filenames)); + assert(ret_parameters_measured); + + for (size_t i = 0; i < n_dts; ++i) { + err = devicetree_install_from_memory(dt_state, dt_bases[i], dt_sizes[i]); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading addon devicetree, ignoring: %m"); + else { + bool m = false; + + err = tpm_log_tagged_event( + TPM2_PCR_KERNEL_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(dt_bases[i]), + dt_sizes[i], + DEVICETREE_ADDON_EVENT_TAG_ID, + dt_filenames[i], + &m); + if (err != EFI_SUCCESS) + return (void) log_error_status( + err, + "Unable to add measurement of DTB addon #%zu to PCR %i: %m", + i, + TPM2_PCR_KERNEL_CONFIG); + + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + } + } + + *ret_parameters_measured = parameters_measured; +} + +static void dt_bases_free(void **dt_bases, size_t n_dt) { + assert(dt_bases || n_dt == 0); + + for (size_t i = 0; i < n_dt; ++i) + free(dt_bases[i]); + + free(dt_bases); +} + +static void dt_filenames_free(char16_t **dt_filenames, size_t n_dt) { + assert(dt_filenames || n_dt == 0); + + for (size_t i = 0; i < n_dt; ++i) + free(dt_filenames[i]); + + free(dt_filenames); +} + +static EFI_STATUS load_addons( + EFI_HANDLE stub_image, + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *prefix, + const char *uname, + char16_t **ret_cmdline, + void ***ret_dt_bases, + size_t **ret_dt_sizes, + char16_t ***ret_dt_filenames, + size_t *ret_n_dt) { + + _cleanup_free_ size_t *dt_sizes = NULL; + _cleanup_(strv_freep) char16_t **items = NULL; + _cleanup_(file_closep) EFI_FILE *root = NULL; + _cleanup_free_ char16_t *cmdline = NULL; + size_t n_items = 0, n_allocated = 0, n_dt = 0; + char16_t **dt_filenames = NULL; + void **dt_bases = NULL; + EFI_STATUS err; + + assert(stub_image); + assert(loaded_image); + assert(prefix); + assert(!!ret_dt_bases == !!ret_dt_sizes); + assert(!!ret_dt_bases == !!ret_n_dt); + assert(!!ret_dt_filenames == !!ret_n_dt); + + if (!loaded_image->DeviceHandle) + return EFI_SUCCESS; + + CLEANUP_ARRAY(dt_bases, n_dt, dt_bases_free); + CLEANUP_ARRAY(dt_filenames, n_dt, dt_filenames_free); + + err = open_volume(loaded_image->DeviceHandle, &root); + if (err == EFI_UNSUPPORTED) + /* Error will be unsupported if the bootloader doesn't implement the file system protocol on + * its file handles. */ + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + err = load_addons_from_dir(root, prefix, &items, &n_items, &n_allocated); + if (err != EFI_SUCCESS) + return err; + + if (n_items == 0) + return EFI_SUCCESS; /* Empty directory */ + + /* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements + * are not dependent on read order) */ + sort_pointer_array((void**) items, n_items, (compare_pointer_func_t) strcmp16); + + for (size_t i = 0; i < n_items; i++) { + size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {}; + _cleanup_free_ EFI_DEVICE_PATH *addon_path = NULL; + _cleanup_(unload_imagep) EFI_HANDLE addon = NULL; + EFI_LOADED_IMAGE_PROTOCOL *loaded_addon = NULL; + _cleanup_free_ char16_t *addon_spath = NULL; + + addon_spath = xasprintf("%ls\\%ls", prefix, items[i]); + err = make_file_device_path(loaded_image->DeviceHandle, addon_spath, &addon_path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making device path for %ls: %m", addon_spath); + + /* By using shim_load_image, we cover both the case where the PE files are signed with MoK + * and with DB, and running with or without shim. */ + err = shim_load_image(stub_image, addon_path, &addon); + if (err != EFI_SUCCESS) { + log_error_status(err, + "Failed to read '%ls' from '%ls', ignoring: %m", + items[i], + addon_spath); + continue; + } + + err = BS->HandleProtocol(addon, + MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), + (void **) &loaded_addon); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to find protocol in %ls: %m", items[i]); + + err = pe_memory_locate_sections(loaded_addon->ImageBase, unified_sections, addrs, szs); + if (err != EFI_SUCCESS || + (szs[UNIFIED_SECTION_CMDLINE] == 0 && szs[UNIFIED_SECTION_DTB] == 0)) { + if (err == EFI_SUCCESS) + err = EFI_NOT_FOUND; + log_error_status(err, + "Unable to locate embedded .cmdline/.dtb sections in %ls, ignoring: %m", + items[i]); + continue; + } + + /* We want to enforce that addons are not UKIs, i.e.: they must not embed a kernel. */ + if (szs[UNIFIED_SECTION_LINUX] > 0) { + log_error_status(EFI_INVALID_PARAMETER, "%ls is a UKI, not an addon, ignoring: %m", items[i]); + continue; + } + + /* Also enforce that, in case it is specified, .uname matches as a quick way to allow + * enforcing compatibility with a specific UKI only */ + if (uname && szs[UNIFIED_SECTION_UNAME] > 0 && + !strneq8(uname, + (char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_UNAME], + szs[UNIFIED_SECTION_UNAME])) { + log_error(".uname mismatch between %ls and UKI, ignoring", items[i]); + continue; + } + + if (ret_cmdline && szs[UNIFIED_SECTION_CMDLINE] > 0) { + _cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), + *extra16 = xstrn8_to_16((char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_CMDLINE], + szs[UNIFIED_SECTION_CMDLINE]); + cmdline = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", extra16); + } + + if (ret_dt_bases && szs[UNIFIED_SECTION_DTB] > 0) { + dt_sizes = xrealloc(dt_sizes, + n_dt * sizeof(size_t), + (n_dt + 1) * sizeof(size_t)); + dt_sizes[n_dt] = szs[UNIFIED_SECTION_DTB]; + + dt_bases = xrealloc(dt_bases, + n_dt * sizeof(void *), + (n_dt + 1) * sizeof(void *)); + dt_bases[n_dt] = xmemdup((uint8_t*)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_DTB], + dt_sizes[n_dt]); + + dt_filenames = xrealloc(dt_filenames, + n_dt * sizeof(char16_t *), + (n_dt + 1) * sizeof(char16_t *)); + dt_filenames[n_dt] = xstrdup16(items[i]); + + ++n_dt; + } + } + + if (ret_cmdline && !isempty(cmdline)) + *ret_cmdline = TAKE_PTR(cmdline); + + if (ret_n_dt && n_dt > 0) { + *ret_dt_filenames = TAKE_PTR(dt_filenames); + *ret_dt_bases = TAKE_PTR(dt_bases); + *ret_dt_sizes = TAKE_PTR(dt_sizes); + *ret_n_dt = n_dt; + } + + return EFI_SUCCESS; +} + +static EFI_STATUS run(EFI_HANDLE image) { + _cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL, *sysext_initrd = NULL, *pcrsig_initrd = NULL, *pcrpkey_initrd = NULL; + size_t credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0, pcrsig_initrd_size = 0, pcrpkey_initrd_size = 0; + void **dt_bases_addons_global = NULL, **dt_bases_addons_uki = NULL; + char16_t **dt_filenames_addons_global = NULL, **dt_filenames_addons_uki = NULL; + _cleanup_free_ size_t *dt_sizes_addons_global = NULL, *dt_sizes_addons_uki = NULL; + size_t linux_size, initrd_size, dt_size, n_dts_addons_global = 0, n_dts_addons_uki = 0; + EFI_PHYSICAL_ADDRESS linux_base, initrd_base, dt_base; + _cleanup_(devicetree_cleanup) struct devicetree_state dt_state = {}; + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {}; + _cleanup_free_ char16_t *cmdline = NULL, *cmdline_addons_global = NULL, *cmdline_addons_uki = NULL; + int sections_measured = -1, parameters_measured = -1; + _cleanup_free_ char *uname = NULL; + bool sysext_measured = false, m; + uint64_t loader_features = 0; + EFI_STATUS err; + + err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); + + if (loaded_image->DeviceHandle && /* Handle case, where bootloader doesn't support DeviceHandle. */ + (efivar_get_uint64_le(MAKE_GUID_PTR(LOADER), u"LoaderFeatures", &loader_features) != EFI_SUCCESS || + !FLAGS_SET(loader_features, EFI_LOADER_FEATURE_RANDOM_SEED))) { + _cleanup_(file_closep) EFI_FILE *esp_dir = NULL; + + err = partition_open(MAKE_GUID_PTR(ESP), loaded_image->DeviceHandle, NULL, &esp_dir); + if (err == EFI_SUCCESS) /* Non-fatal on failure, so that we still boot without it. */ + (void) process_random_seed(esp_dir); + } + + err = pe_memory_locate_sections(loaded_image->ImageBase, unified_sections, addrs, szs); + if (err != EFI_SUCCESS || szs[UNIFIED_SECTION_LINUX] == 0) { + if (err == EFI_SUCCESS) + err = EFI_NOT_FOUND; + return log_error_status(err, "Unable to locate embedded .linux section: %m"); + } + + CLEANUP_ARRAY(dt_bases_addons_global, n_dts_addons_global, dt_bases_free); + CLEANUP_ARRAY(dt_bases_addons_uki, n_dts_addons_uki, dt_bases_free); + CLEANUP_ARRAY(dt_filenames_addons_global, n_dts_addons_global, dt_filenames_free); + CLEANUP_ARRAY(dt_filenames_addons_uki, n_dts_addons_uki, dt_filenames_free); + + /* Now that we have the UKI sections loaded, also load global first and then local (per-UKI) + * addons. The data is loaded at once, and then used later. */ + err = load_addons( + image, + loaded_image, + u"\\loader\\addons", + uname, + &cmdline_addons_global, + &dt_bases_addons_global, + &dt_sizes_addons_global, + &dt_filenames_addons_global, + &n_dts_addons_global); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading global addons, ignoring: %m"); + + /* Some bootloaders always pass NULL in FilePath, so we need to check for it here. */ + _cleanup_free_ char16_t *dropin_dir = get_extra_dir(loaded_image->FilePath); + if (dropin_dir) { + err = load_addons( + image, + loaded_image, + dropin_dir, + uname, + &cmdline_addons_uki, + &dt_bases_addons_uki, + &dt_sizes_addons_uki, + &dt_filenames_addons_uki, + &n_dts_addons_uki); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading UKI-specific addons, ignoring: %m"); + } + + /* Measure all "payload" of this PE image into a separate PCR (i.e. where nothing else is written + * into so far), so that we have one PCR that we can nicely write policies against because it + * contains all static data of this image, and thus can be easily be pre-calculated. */ + for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) { + + if (!unified_section_measure(section)) /* shall not measure? */ + continue; + + if (szs[section] == 0) /* not found */ + continue; + + m = false; + + /* First measure the name of the section */ + (void) tpm_log_event_ascii( + TPM2_PCR_KERNEL_BOOT, + POINTER_TO_PHYSICAL_ADDRESS(unified_sections[section]), + strsize8(unified_sections[section]), /* including NUL byte */ + unified_sections[section], + &m); + + sections_measured = sections_measured < 0 ? m : (sections_measured && m); + + /* Then measure the data of the section */ + (void) tpm_log_event_ascii( + TPM2_PCR_KERNEL_BOOT, + POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[section], + szs[section], + unified_sections[section], + &m); + + sections_measured = sections_measured < 0 ? m : (sections_measured && m); + } + + /* After we are done, set an EFI variable that tells userspace this was done successfully, and encode + * in it which PCR was used. */ + if (sections_measured > 0) + (void) efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"StubPcrKernelImage", TPM2_PCR_KERNEL_BOOT, 0); + + /* Show splash screen as early as possible */ + graphics_splash((const uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_SPLASH], szs[UNIFIED_SECTION_SPLASH]); + + if (szs[UNIFIED_SECTION_UNAME] > 0) + uname = xstrndup8((char *)loaded_image->ImageBase + addrs[UNIFIED_SECTION_UNAME], + szs[UNIFIED_SECTION_UNAME]); + + if (use_load_options(image, loaded_image, szs[UNIFIED_SECTION_CMDLINE] > 0, &cmdline)) { + /* Let's measure the passed kernel command line into the TPM. Note that this possibly + * duplicates what we already did in the boot menu, if that was already used. However, since + * we want the boot menu to support an EFI binary, and want to this stub to be usable from + * any boot menu, let's measure things anyway. */ + m = false; + (void) tpm_log_load_options(cmdline, &m); + parameters_measured = m; + } else if (szs[UNIFIED_SECTION_CMDLINE] > 0) { + cmdline = xstrn8_to_16( + (char *) loaded_image->ImageBase + addrs[UNIFIED_SECTION_CMDLINE], + szs[UNIFIED_SECTION_CMDLINE]); + mangle_stub_cmdline(cmdline); + } + + /* If we have any extra command line to add via PE addons, load them now and append, and + * measure the additions together, after the embedded options, but before the smbios ones, + * so that the order is reversed from "most hardcoded" to "most dynamic". The global addons are + * loaded first, and the image-specific ones later, for the same reason. */ + cmdline_append_and_measure_addons(cmdline_addons_global, cmdline_addons_uki, &cmdline, &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + /* SMBIOS OEM Strings data is controlled by the host admin and not covered + * by the VM attestation, so MUST NOT be trusted when in a confidential VM */ + if (!is_confidential_vm()) { + const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra"); + if (extra) { + _cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), *extra16 = xstr8_to_16(extra); + cmdline = xasprintf("%ls %ls", tmp, extra16); + + /* SMBIOS strings are measured in PCR1, but we also want to measure them in our specific + * PCR12, as firmware-owned PCRs are very difficult to use as they'll contain unpredictable + * measurements that are not under control of the machine owner. */ + m = false; + (void) tpm_log_load_options(extra16, &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + } + } + + export_variables(loaded_image); + + if (pack_cpio(loaded_image, + NULL, + u".cred", + ".extra/credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG, + u"Credentials initrd", + &credential_initrd, + &credential_initrd_size, + &m) == EFI_SUCCESS) + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (pack_cpio(loaded_image, + u"\\loader\\credentials", + u".cred", + ".extra/global_credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG, + u"Global credentials initrd", + &global_credential_initrd, + &global_credential_initrd_size, + &m) == EFI_SUCCESS) + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (pack_cpio(loaded_image, + NULL, + u".raw", + ".extra/sysext", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ TPM2_PCR_SYSEXTS, + u"System extension initrd", + &sysext_initrd, + &sysext_initrd_size, + &m) == EFI_SUCCESS) + sysext_measured = m; + + dt_size = szs[UNIFIED_SECTION_DTB]; + dt_base = dt_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_DTB] : 0; + + /* First load the base device tree, then fix it up using addons - global first, then per-UKI. */ + if (dt_size > 0) { + err = devicetree_install_from_memory( + &dt_state, PHYSICAL_ADDRESS_TO_POINTER(dt_base), dt_size); + if (err != EFI_SUCCESS) + log_error_status(err, "Error loading embedded devicetree: %m"); + } + + dtb_install_addons(&dt_state, + dt_bases_addons_global, + dt_sizes_addons_global, + dt_filenames_addons_global, + n_dts_addons_global, + &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + dtb_install_addons(&dt_state, + dt_bases_addons_uki, + dt_sizes_addons_uki, + dt_filenames_addons_uki, + n_dts_addons_uki, + &m); + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (parameters_measured > 0) + (void) efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"StubPcrKernelParameters", TPM2_PCR_KERNEL_CONFIG, 0); + if (sysext_measured) + (void) efivar_set_uint_string(MAKE_GUID_PTR(LOADER), u"StubPcrInitRDSysExts", TPM2_PCR_SYSEXTS, 0); + + /* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it + * to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section + * is not measured, neither as raw section (see above), nor as cpio (here), because it is the + * signature of expected PCR values, i.e. its input are PCR measurements, and hence it shouldn't + * itself be input for PCR measurements. */ + if (szs[UNIFIED_SECTION_PCRSIG] > 0) + (void) pack_cpio_literal( + (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRSIG], + szs[UNIFIED_SECTION_PCRSIG], + ".extra", + u"tpm2-pcr-signature.json", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ UINT32_MAX, + /* tpm_description= */ NULL, + &pcrsig_initrd, + &pcrsig_initrd_size, + /* ret_measured= */ NULL); + + /* If the public key used for the PCR signatures was embedded in the PE image, then let's wrap it in + * a cpio and also pass it to the kernel, so that it can be read from + * /.extra/tpm2-pcr-public-key.pem. This section is already measure above, hence we won't measure the + * cpio. */ + if (szs[UNIFIED_SECTION_PCRPKEY] > 0) + (void) pack_cpio_literal( + (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRPKEY], + szs[UNIFIED_SECTION_PCRPKEY], + ".extra", + u"tpm2-pcr-public-key.pem", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ UINT32_MAX, + /* tpm_description= */ NULL, + &pcrpkey_initrd, + &pcrpkey_initrd_size, + /* ret_measured= */ NULL); + + linux_size = szs[UNIFIED_SECTION_LINUX]; + linux_base = POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_LINUX]; + + initrd_size = szs[UNIFIED_SECTION_INITRD]; + initrd_base = initrd_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_INITRD] : 0; + + _cleanup_pages_ Pages initrd_pages = {}; + if (credential_initrd || global_credential_initrd || sysext_initrd || pcrsig_initrd || pcrpkey_initrd) { + /* If we have generated initrds dynamically, let's combine them with the built-in initrd. */ + err = combine_initrd( + initrd_base, initrd_size, + (const void*const[]) { + credential_initrd, + global_credential_initrd, + sysext_initrd, + pcrsig_initrd, + pcrpkey_initrd, + }, + (const size_t[]) { + credential_initrd_size, + global_credential_initrd_size, + sysext_initrd_size, + pcrsig_initrd_size, + pcrpkey_initrd_size, + }, + 5, + &initrd_pages, &initrd_size); + if (err != EFI_SUCCESS) + return err; + + initrd_base = initrd_pages.addr; + + /* Given these might be large let's free them explicitly, quickly. */ + credential_initrd = mfree(credential_initrd); + global_credential_initrd = mfree(global_credential_initrd); + sysext_initrd = mfree(sysext_initrd); + pcrsig_initrd = mfree(pcrsig_initrd); + pcrpkey_initrd = mfree(pcrpkey_initrd); + } + + err = linux_exec(image, cmdline, + PHYSICAL_ADDRESS_TO_POINTER(linux_base), linux_size, + PHYSICAL_ADDRESS_TO_POINTER(initrd_base), initrd_size); + graphics_mode(false); + return err; +} + +DEFINE_EFI_MAIN_FUNCTION(run, "systemd-stub", /*wait_for_debugger=*/false); diff --git a/src/boot/efi/test-bcd.c b/src/boot/efi/test-bcd.c new file mode 100644 index 0000000..3f93ca0 --- /dev/null +++ b/src/boot/efi/test-bcd.c @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bcd.h" +#include "compress.h" +#include "fileio.h" +#include "tests.h" +#include "utf8.h" + +/* Include the implementation directly, so we can poke at some internals. */ +#include "bcd.c" + +static void load_bcd(const char *path, void **ret_bcd, size_t *ret_bcd_len) { + size_t len; + _cleanup_free_ char *fn = NULL, *compressed = NULL; + + assert_se(get_testdata_dir(path, &fn) >= 0); + assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, SIZE_MAX, 0, NULL, &compressed, &len) >= 0); + assert_se(decompress_blob_zstd(compressed, len, ret_bcd, ret_bcd_len, SIZE_MAX) >= 0); +} + +static void test_get_bcd_title_one( + const char *path, + const char16_t *title_expect, + size_t title_len_expect) { + + size_t len; + _cleanup_free_ void *bcd = NULL; + + log_info("/* %s(%s) */", __func__, path); + + load_bcd(path, &bcd, &len); + + char16_t *title = get_bcd_title(bcd, len); + if (title_expect) { + assert_se(title); + assert_se(memcmp(title, title_expect, title_len_expect) == 0); + } else + assert_se(!title); +} + +TEST(get_bcd_title) { + test_get_bcd_title_one("test-bcd/win10.bcd.zst", u"Windows 10", sizeof(u"Windows 10")); + + test_get_bcd_title_one("test-bcd/description-bad-type.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/description-empty.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/description-missing.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/description-too-small.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/displayorder-bad-name.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/displayorder-bad-size.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/displayorder-bad-type.bcd.zst", NULL, 0); + test_get_bcd_title_one("test-bcd/empty.bcd.zst", NULL, 0); +} + +TEST(base_block) { + size_t len; + BaseBlock backup; + uint8_t *bcd_base; + _cleanup_free_ BaseBlock *bcd = NULL; + + load_bcd("test-bcd/win10.bcd.zst", (void **) &bcd, &len); + backup = *bcd; + bcd_base = (uint8_t *) bcd; + + assert_se(get_bcd_title(bcd_base, len)); + + /* Try various "corruptions" of the base block. */ + + assert_se(!get_bcd_title(bcd_base, sizeof(BaseBlock) - 1)); + + bcd->sig = 0; + assert_se(!get_bcd_title(bcd_base, len)); + *bcd = backup; + + bcd->version_minor = 2; + assert_se(!get_bcd_title(bcd_base, len)); + *bcd = backup; + + bcd->version_major = 4; + assert_se(!get_bcd_title(bcd_base, len)); + *bcd = backup; + + bcd->type = 1; + assert_se(!get_bcd_title(bcd_base, len)); + *bcd = backup; + + bcd->primary_seqnum++; + assert_se(!get_bcd_title(bcd_base, len)); + *bcd = backup; +} + +TEST(bad_bcd) { + size_t len; + uint8_t *hbins; + uint32_t offset; + _cleanup_free_ void *bcd = NULL; + + /* This BCD hive has been manipulated to have bad offsets/sizes at various places. */ + load_bcd("test-bcd/corrupt.bcd.zst", &bcd, &len); + + assert_se(len >= HIVE_CELL_OFFSET); + hbins = (uint8_t *) bcd + HIVE_CELL_OFFSET; + len -= HIVE_CELL_OFFSET; + offset = ((BaseBlock *) bcd)->root_cell_offset; + + const Key *root = get_key(hbins, len, offset, "\0"); + assert_se(root); + assert_se(!get_key(hbins, sizeof(Key) - 1, offset, "\0")); + + assert_se(!get_key(hbins, len, offset, "\0BadOffset\0")); + assert_se(!get_key(hbins, len, offset, "\0BadSig\0")); + assert_se(!get_key(hbins, len, offset, "\0BadKeyNameLen\0")); + assert_se(!get_key(hbins, len, offset, "\0SubkeyBadOffset\0Dummy\0")); + assert_se(!get_key(hbins, len, offset, "\0SubkeyBadSig\0Dummy\0")); + assert_se(!get_key(hbins, len, offset, "\0SubkeyBadNEntries\0Dummy\0")); + + assert_se(!get_key_value(hbins, len, root, "Dummy")); + + const Key *kv_bad_offset = get_key(hbins, len, offset, "\0KeyValuesBadOffset\0"); + assert_se(kv_bad_offset); + assert_se(!get_key_value(hbins, len, kv_bad_offset, "Dummy")); + + const Key *kv_bad_n_key_values = get_key(hbins, len, offset, "\0KeyValuesBadNKeyValues\0"); + assert_se(kv_bad_n_key_values); + assert_se(!get_key_value(hbins, len, kv_bad_n_key_values, "Dummy")); + + const Key *kv = get_key(hbins, len, offset, "\0KeyValues\0"); + assert_se(kv); + + assert_se(!get_key_value(hbins, len, kv, "BadOffset")); + assert_se(!get_key_value(hbins, len, kv, "BadSig")); + assert_se(!get_key_value(hbins, len, kv, "BadNameLen")); + assert_se(!get_key_value(hbins, len, kv, "InlineData")); + assert_se(!get_key_value(hbins, len, kv, "BadDataOffset")); + assert_se(!get_key_value(hbins, len, kv, "BadDataSize")); +} + +TEST(argv_bcds) { + for (int i = 1; i < saved_argc; i++) { + size_t len; + _cleanup_free_ void *bcd = NULL; + + assert_se(read_full_file_full( + AT_FDCWD, + saved_argv[i], + UINT64_MAX, + SIZE_MAX, + 0, + NULL, + (char **) &bcd, + &len) >= 0); + + char16_t *title = get_bcd_title(bcd, len); + if (title) { + _cleanup_free_ char *title_utf8 = utf16_to_utf8(title, SIZE_MAX); + log_info("%s: \"%s\"", saved_argv[i], title_utf8); + } else + log_info("%s: Bad BCD", saved_argv[i]); + } +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/boot/efi/test-efi-string.c b/src/boot/efi/test-efi-string.c new file mode 100644 index 0000000..b71a0c3 --- /dev/null +++ b/src/boot/efi/test-efi-string.c @@ -0,0 +1,794 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "efi-string.h" +#include "fileio.h" +#include "tests.h" + +TEST(strlen8) { + assert_se(strlen8(NULL) == 0); + assert_se(strlen8("") == 0); + assert_se(strlen8("1") == 1); + assert_se(strlen8("11") == 2); + assert_se(strlen8("123456789") == 9); + assert_se(strlen8("12\0004") == 2); +} + +TEST(strlen16) { + assert_se(strlen16(NULL) == 0); + assert_se(strlen16(u"") == 0); + assert_se(strlen16(u"1") == 1); + assert_se(strlen16(u"11") == 2); + assert_se(strlen16(u"123456789") == 9); + assert_se(strlen16(u"12\0004") == 2); +} + +TEST(strnlen8) { + assert_se(strnlen8(NULL, 0) == 0); + assert_se(strnlen8(NULL, 10) == 0); + assert_se(strnlen8("", 10) == 0); + assert_se(strnlen8("1", 10) == 1); + assert_se(strnlen8("11", 1) == 1); + assert_se(strnlen8("123456789", 7) == 7); + assert_se(strnlen8("12\0004", 5) == 2); +} + +TEST(strnlen16) { + assert_se(strnlen16(NULL, 0) == 0); + assert_se(strnlen16(NULL, 10) == 0); + assert_se(strnlen16(u"", 10) == 0); + assert_se(strnlen16(u"1", 10) == 1); + assert_se(strnlen16(u"11", 1) == 1); + assert_se(strnlen16(u"123456789", 7) == 7); + assert_se(strnlen16(u"12\0004", 5) == 2); +} + +TEST(strsize8) { + assert_se(strsize8(NULL) == 0); + assert_se(strsize8("") == 1); + assert_se(strsize8("1") == 2); + assert_se(strsize8("11") == 3); + assert_se(strsize8("123456789") == 10); + assert_se(strsize8("12\0004") == 3); +} + +TEST(strsize16) { + assert_se(strsize16(NULL) == 0); + assert_se(strsize16(u"") == 2); + assert_se(strsize16(u"1") == 4); + assert_se(strsize16(u"11") == 6); + assert_se(strsize16(u"123456789") == 20); + assert_se(strsize16(u"12\0004") == 6); +} + +TEST(strtolower8) { + char s[] = "\0001234abcDEF!\0zZ"; + + strtolower8(NULL); + + strtolower8(s); + assert_se(memcmp(s, "\0001234abcDEF!\0zZ", sizeof(s)) == 0); + + s[0] = '#'; + strtolower8(s); + assert_se(memcmp(s, "#1234abcdef!\0zZ", sizeof(s)) == 0); +} + +TEST(strtolower16) { + char16_t s[] = u"\0001234abcDEF!\0zZ"; + + strtolower16(NULL); + + strtolower16(s); + assert_se(memcmp(s, u"\0001234abcDEF!\0zZ", sizeof(s)) == 0); + + s[0] = '#'; + strtolower16(s); + assert_se(memcmp(s, u"#1234abcdef!\0zZ", sizeof(s)) == 0); +} + +TEST(strncmp8) { + assert_se(strncmp8(NULL, "", 10) < 0); + assert_se(strncmp8("", NULL, 10) > 0); + assert_se(strncmp8(NULL, NULL, 0) == 0); + assert_se(strncmp8(NULL, NULL, 10) == 0); + assert_se(strncmp8("", "", 10) == 0); + assert_se(strncmp8("abc", "abc", 2) == 0); + assert_se(strncmp8("aBc", "aBc", 3) == 0); + assert_se(strncmp8("aBC", "aBC", 4) == 0); + assert_se(strncmp8("", "a", 0) == 0); + assert_se(strncmp8("b", "a", 0) == 0); + assert_se(strncmp8("", "a", 3) < 0); + assert_se(strncmp8("=", "=", 1) == 0); + assert_se(strncmp8("A", "a", 1) < 0); + assert_se(strncmp8("a", "A", 2) > 0); + assert_se(strncmp8("a", "Aa", 2) > 0); + assert_se(strncmp8("12\00034", "12345", 4) < 0); + assert_se(strncmp8("12\00034", "12345", SIZE_MAX) < 0); + assert_se(strncmp8("abc\0def", "abc", SIZE_MAX) == 0); + assert_se(strncmp8("abc\0def", "abcdef", SIZE_MAX) < 0); + + assert_se(strncmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MIN }, 1) == 0); + assert_se(strncmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MAX }, 1) == 0); + assert_se(strncmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MAX }, 1) < 0); + assert_se(strncmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MIN }, 1) > 0); +} + +TEST(strncmp16) { + assert_se(strncmp16(NULL, u"", 10) < 0); + assert_se(strncmp16(u"", NULL, 10) > 0); + assert_se(strncmp16(NULL, NULL, 0) == 0); + assert_se(strncmp16(NULL, NULL, 10) == 0); + assert_se(strncmp16(u"", u"", 0) == 0); + assert_se(strncmp16(u"", u"", 10) == 0); + assert_se(strncmp16(u"abc", u"abc", 2) == 0); + assert_se(strncmp16(u"aBc", u"aBc", 3) == 0); + assert_se(strncmp16(u"aBC", u"aBC", 4) == 0); + assert_se(strncmp16(u"", u"a", 0) == 0); + assert_se(strncmp16(u"b", u"a", 0) == 0); + assert_se(strncmp16(u"", u"a", 3) < 0); + assert_se(strncmp16(u"=", u"=", 1) == 0); + assert_se(strncmp16(u"A", u"a", 1) < 0); + assert_se(strncmp16(u"a", u"A", 2) > 0); + assert_se(strncmp16(u"a", u"Aa", 2) > 0); + assert_se(strncmp16(u"12\00034", u"12345", 4) < 0); + assert_se(strncmp16(u"12\00034", u"12345", SIZE_MAX) < 0); + assert_se(strncmp16(u"abc\0def", u"abc", SIZE_MAX) == 0); + assert_se(strncmp16(u"abc\0def", u"abcdef", SIZE_MAX) < 0); + + assert_se(strncmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ UINT16_MAX }, 1) == 0); + assert_se(strncmp16((char16_t[]){ 0 }, (char16_t[]){ UINT16_MAX }, 1) < 0); + assert_se(strncmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ 0 }, 1) > 0); +} + +TEST(strncasecmp8) { + assert_se(strncasecmp8(NULL, "", 10) < 0); + assert_se(strncasecmp8("", NULL, 10) > 0); + assert_se(strncasecmp8(NULL, NULL, 0) == 0); + assert_se(strncasecmp8(NULL, NULL, 10) == 0); + assert_se(strncasecmp8("", "", 10) == 0); + assert_se(strncasecmp8("abc", "abc", 2) == 0); + assert_se(strncasecmp8("aBc", "AbC", 3) == 0); + assert_se(strncasecmp8("aBC", "Abc", 4) == 0); + assert_se(strncasecmp8("", "a", 0) == 0); + assert_se(strncasecmp8("b", "a", 0) == 0); + assert_se(strncasecmp8("", "a", 3) < 0); + assert_se(strncasecmp8("=", "=", 1) == 0); + assert_se(strncasecmp8("A", "a", 1) == 0); + assert_se(strncasecmp8("a", "A", 2) == 0); + assert_se(strncasecmp8("a", "Aa", 2) < 0); + assert_se(strncasecmp8("12\00034", "12345", 4) < 0); + assert_se(strncasecmp8("12\00034", "12345", SIZE_MAX) < 0); + assert_se(strncasecmp8("abc\0def", "ABC", SIZE_MAX) == 0); + assert_se(strncasecmp8("abc\0def", "ABCDEF", SIZE_MAX) < 0); + + assert_se(strncasecmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MIN }, 1) == 0); + assert_se(strncasecmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MAX }, 1) == 0); + assert_se(strncasecmp8((char[]){ CHAR_MIN }, (char[]){ CHAR_MAX }, 1) < 0); + assert_se(strncasecmp8((char[]){ CHAR_MAX }, (char[]){ CHAR_MIN }, 1) > 0); +} + +TEST(strncasecmp16) { + assert_se(strncasecmp16(NULL, u"", 10) < 0); + assert_se(strncasecmp16(u"", NULL, 10) > 0); + assert_se(strncasecmp16(NULL, NULL, 0) == 0); + assert_se(strncasecmp16(NULL, NULL, 10) == 0); + assert_se(strncasecmp16(u"", u"", 10) == 0); + assert_se(strncasecmp16(u"abc", u"abc", 2) == 0); + assert_se(strncasecmp16(u"aBc", u"AbC", 3) == 0); + assert_se(strncasecmp16(u"aBC", u"Abc", 4) == 0); + assert_se(strncasecmp16(u"", u"a", 0) == 0); + assert_se(strncasecmp16(u"b", u"a", 0) == 0); + assert_se(strncasecmp16(u"", u"a", 3) < 0); + assert_se(strncasecmp16(u"=", u"=", 1) == 0); + assert_se(strncasecmp16(u"A", u"a", 1) == 0); + assert_se(strncasecmp16(u"a", u"A", 2) == 0); + assert_se(strncasecmp16(u"a", u"Aa", 2) < 0); + assert_se(strncasecmp16(u"12\00034", u"12345", 4) < 0); + assert_se(strncasecmp16(u"12\00034", u"12345", SIZE_MAX) < 0); + assert_se(strncasecmp16(u"abc\0def", u"ABC", SIZE_MAX) == 0); + assert_se(strncasecmp16(u"abc\0def", u"ABCDEF", SIZE_MAX) < 0); + + assert_se(strncasecmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ UINT16_MAX }, 1) == 0); + assert_se(strncasecmp16((char16_t[]){ 0 }, (char16_t[]){ UINT16_MAX }, 1) < 0); + assert_se(strncasecmp16((char16_t[]){ UINT16_MAX }, (char16_t[]){ 0 }, 1) > 0); +} + +TEST(strcpy8) { + char buf[128]; + + assert_se(strcpy8(buf, "123") == buf); + assert_se(streq8(buf, "123")); + assert_se(strcpy8(buf, "") == buf); + assert_se(streq8(buf, "")); + assert_se(strcpy8(buf, "A") == buf); + assert_se(streq8(buf, "A")); + assert_se(strcpy8(buf, NULL) == buf); + assert_se(streq8(buf, "")); +} + +TEST(strcpy16) { + char16_t buf[128]; + + assert_se(strcpy16(buf, u"123") == buf); + assert_se(streq16(buf, u"123")); + assert_se(strcpy16(buf, u"") == buf); + assert_se(streq16(buf, u"")); + assert_se(strcpy16(buf, u"A") == buf); + assert_se(streq16(buf, u"A")); + assert_se(strcpy16(buf, NULL) == buf); + assert_se(streq16(buf, u"")); +} + +TEST(strchr8) { + assert_se(!strchr8(NULL, 'a')); + assert_se(!strchr8("", 'a')); + assert_se(!strchr8("123", 'a')); + + const char str[] = "abcaBc"; + assert_se(strchr8(str, 'a') == &str[0]); + assert_se(strchr8(str, 'c') == &str[2]); + assert_se(strchr8(str, 'B') == &str[4]); + + assert_se(strchr8(str, 0) == str + strlen8(str)); +} + +TEST(strchr16) { + assert_se(!strchr16(NULL, 'a')); + assert_se(!strchr16(u"", 'a')); + assert_se(!strchr16(u"123", 'a')); + + const char16_t str[] = u"abcaBc"; + assert_se(strchr16(str, 'a') == &str[0]); + assert_se(strchr16(str, 'c') == &str[2]); + assert_se(strchr16(str, 'B') == &str[4]); + + assert_se(strchr16(str, 0) == str + strlen16(str)); +} + +TEST(xstrndup8) { + char *s = NULL; + + assert_se(xstrndup8(NULL, 0) == NULL); + assert_se(xstrndup8(NULL, 10) == NULL); + + assert_se(s = xstrndup8("", 10)); + assert_se(streq8(s, "")); + free(s); + + assert_se(s = xstrndup8("abc", 0)); + assert_se(streq8(s, "")); + free(s); + + assert_se(s = xstrndup8("ABC", 3)); + assert_se(streq8(s, "ABC")); + free(s); + + assert_se(s = xstrndup8("123abcDEF", 5)); + assert_se(streq8(s, "123ab")); + free(s); +} + +TEST(xstrdup8) { + char *s = NULL; + + assert_se(xstrdup8(NULL) == NULL); + + assert_se(s = xstrdup8("")); + assert_se(streq8(s, "")); + free(s); + + assert_se(s = xstrdup8("1")); + assert_se(streq8(s, "1")); + free(s); + + assert_se(s = xstrdup8("123abcDEF")); + assert_se(streq8(s, "123abcDEF")); + free(s); +} + +TEST(xstrndup16) { + char16_t *s = NULL; + + assert_se(xstrndup16(NULL, 0) == NULL); + assert_se(xstrndup16(NULL, 10) == NULL); + + assert_se(s = xstrndup16(u"", 10)); + assert_se(streq16(s, u"")); + free(s); + + assert_se(s = xstrndup16(u"abc", 0)); + assert_se(streq16(s, u"")); + free(s); + + assert_se(s = xstrndup16(u"ABC", 3)); + assert_se(streq16(s, u"ABC")); + free(s); + + assert_se(s = xstrndup16(u"123abcDEF", 5)); + assert_se(streq16(s, u"123ab")); + free(s); +} + +TEST(xstrdup16) { + char16_t *s = NULL; + + assert_se(xstrdup16(NULL) == NULL); + + assert_se(s = xstrdup16(u"")); + assert_se(streq16(s, u"")); + free(s); + + assert_se(s = xstrdup16(u"1")); + assert_se(streq16(s, u"1")); + free(s); + + assert_se(s = xstrdup16(u"123abcDEF")); + assert_se(streq16(s, u"123abcDEF")); + free(s); +} + +TEST(xstrn8_to_16) { + char16_t *s = NULL; + + assert_se(xstrn8_to_16(NULL, 1) == NULL); + assert_se(xstrn8_to_16("a", 0) == NULL); + + assert_se(s = xstrn8_to_16("", 1)); + assert_se(streq16(s, u"")); + free(s); + + assert_se(s = xstrn8_to_16("1", 1)); + assert_se(streq16(s, u"1")); + free(s); + + assert_se(s = xstr8_to_16("abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~")); + assert_se(streq16(s, u"abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~")); + free(s); + + assert_se(s = xstr8_to_16("ÿⱿ𝇉 😺")); + assert_se(streq16(s, u"ÿⱿ ")); + free(s); + + assert_se(s = xstrn8_to_16("¶¶", 3)); + assert_se(streq16(s, u"¶")); + free(s); +} + +TEST(startswith8) { + assert_se(streq8(startswith8("", ""), "")); + assert_se(streq8(startswith8("x", ""), "x")); + assert_se(!startswith8("", "x")); + assert_se(!startswith8("", "xxxxxxxx")); + assert_se(streq8(startswith8("xxx", "x"), "xx")); + assert_se(streq8(startswith8("xxx", "xx"), "x")); + assert_se(streq8(startswith8("xxx", "xxx"), "")); + assert_se(!startswith8("xxx", "xxxx")); + assert_se(!startswith8(NULL, "")); +} + +#define TEST_FNMATCH_ONE(pattern, haystack, expect) \ + ({ \ + assert_se(fnmatch(pattern, haystack, 0) == (expect ? 0 : FNM_NOMATCH)); \ + assert_se(efi_fnmatch(u##pattern, u##haystack) == expect); \ + }) + +TEST(efi_fnmatch) { + TEST_FNMATCH_ONE("", "", true); + TEST_FNMATCH_ONE("abc", "abc", true); + TEST_FNMATCH_ONE("aBc", "abc", false); + TEST_FNMATCH_ONE("b", "a", false); + TEST_FNMATCH_ONE("b", "", false); + TEST_FNMATCH_ONE("abc", "a", false); + TEST_FNMATCH_ONE("a?c", "azc", true); + TEST_FNMATCH_ONE("???", "?.9", true); + TEST_FNMATCH_ONE("1?", "1", false); + TEST_FNMATCH_ONE("***", "", true); + TEST_FNMATCH_ONE("*", "123", true); + TEST_FNMATCH_ONE("**", "abcd", true); + TEST_FNMATCH_ONE("*b*", "abcd", true); + TEST_FNMATCH_ONE("abc*d", "abc", false); + TEST_FNMATCH_ONE("start*end", "startend", true); + TEST_FNMATCH_ONE("start*end", "startendend", true); + TEST_FNMATCH_ONE("start*end", "startenddne", false); + TEST_FNMATCH_ONE("start*end", "startendstartend", true); + TEST_FNMATCH_ONE("start*end", "starten", false); + TEST_FNMATCH_ONE("*.conf", "arch.conf", true); + TEST_FNMATCH_ONE("debian-*.conf", "debian-wheezy.conf", true); + TEST_FNMATCH_ONE("debian-*.*", "debian-wheezy.efi", true); + TEST_FNMATCH_ONE("ab*cde", "abzcd", false); + TEST_FNMATCH_ONE("\\*\\a\\[", "*a[", true); + TEST_FNMATCH_ONE("[abc] [abc] [abc]", "a b c", true); + TEST_FNMATCH_ONE("abc]", "abc]", true); + TEST_FNMATCH_ONE("[abc]", "z", false); + TEST_FNMATCH_ONE("[abc", "a", false); + TEST_FNMATCH_ONE("[][!] [][!] [][!]", "[ ] !", true); + TEST_FNMATCH_ONE("[]-] []-]", "] -", true); + TEST_FNMATCH_ONE("[1\\]] [1\\]]", "1 ]", true); + TEST_FNMATCH_ONE("[$-\\+]", "&", true); + TEST_FNMATCH_ONE("[1-3A-C] [1-3A-C]", "2 B", true); + TEST_FNMATCH_ONE("[3-5] [3-5] [3-5]", "3 4 5", true); + TEST_FNMATCH_ONE("[f-h] [f-h] [f-h]", "f g h", true); + TEST_FNMATCH_ONE("[a-c-f] [a-c-f] [a-c-f] [a-c-f] [a-c-f]", "a b c - f", true); + TEST_FNMATCH_ONE("[a-c-f]", "e", false); + TEST_FNMATCH_ONE("[--0] [--0] [--0]", "- . 0", true); + TEST_FNMATCH_ONE("[+--] [+--] [+--]", "+ , -", true); + TEST_FNMATCH_ONE("[f-l]", "m", false); + TEST_FNMATCH_ONE("[b]", "z-a", false); + TEST_FNMATCH_ONE("[a\\-z]", "b", false); + TEST_FNMATCH_ONE("?a*b[.-0]c", "/a/b/c", true); + TEST_FNMATCH_ONE("debian-*-*-*.*", "debian-jessie-2018-06-17-kernel-image-5.10.0-16-amd64.efi", true); + + /* These would take forever with a backtracking implementation. */ + TEST_FNMATCH_ONE( + "a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*", + "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyy", + false); + TEST_FNMATCH_ONE( + "a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*", + "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyyzzzz!!!!", + true); +} + +TEST(parse_number8) { + uint64_t u; + const char *tail; + + assert_se(!parse_number8(NULL, &u, NULL)); + assert_se(!parse_number8("", &u, NULL)); + assert_se(!parse_number8("a1", &u, NULL)); + assert_se(!parse_number8("1a", &u, NULL)); + assert_se(!parse_number8("-42", &u, NULL)); + assert_se(!parse_number8("18446744073709551616", &u, NULL)); + + assert_se(parse_number8("0", &u, NULL)); + assert_se(u == 0); + assert_se(parse_number8("1", &u, NULL)); + assert_se(u == 1); + assert_se(parse_number8("999", &u, NULL)); + assert_se(u == 999); + assert_se(parse_number8("18446744073709551615", &u, NULL)); + assert_se(u == UINT64_MAX); + assert_se(parse_number8("42", &u, &tail)); + assert_se(u == 42); + assert_se(streq8(tail, "")); + assert_se(parse_number8("54321rest", &u, &tail)); + assert_se(u == 54321); + assert_se(streq8(tail, "rest")); +} + +TEST(parse_number16) { + uint64_t u; + const char16_t *tail; + + assert_se(!parse_number16(NULL, &u, NULL)); + assert_se(!parse_number16(u"", &u, NULL)); + assert_se(!parse_number16(u"a1", &u, NULL)); + assert_se(!parse_number16(u"1a", &u, NULL)); + assert_se(!parse_number16(u"-42", &u, NULL)); + assert_se(!parse_number16(u"18446744073709551616", &u, NULL)); + + assert_se(parse_number16(u"0", &u, NULL)); + assert_se(u == 0); + assert_se(parse_number16(u"1", &u, NULL)); + assert_se(u == 1); + assert_se(parse_number16(u"999", &u, NULL)); + assert_se(u == 999); + assert_se(parse_number16(u"18446744073709551615", &u, NULL)); + assert_se(u == UINT64_MAX); + assert_se(parse_number16(u"42", &u, &tail)); + assert_se(u == 42); + assert_se(streq16(tail, u"")); + assert_se(parse_number16(u"54321rest", &u, &tail)); + assert_se(u == 54321); + assert_se(streq16(tail, u"rest")); +} + +TEST(parse_boolean) { + bool b; + + assert_se(!parse_boolean(NULL, &b)); + assert_se(!parse_boolean("", &b)); + assert_se(!parse_boolean("ja", &b)); + assert_se(parse_boolean("1", &b) && b == true); + assert_se(parse_boolean("y", &b) && b == true); + assert_se(parse_boolean("yes", &b) && b == true); + assert_se(parse_boolean("t", &b) && b == true); + assert_se(parse_boolean("true", &b) && b == true); + assert_se(parse_boolean("on", &b) && b == true); + assert_se(parse_boolean("0", &b) && b == false); + assert_se(parse_boolean("n", &b) && b == false); + assert_se(parse_boolean("no", &b) && b == false); + assert_se(parse_boolean("f", &b) && b == false); + assert_se(parse_boolean("false", &b) && b == false); + assert_se(parse_boolean("off", &b) && b == false); +} + +TEST(line_get_key_value) { + char s1[] = "key=value\n" + " \t # comment line \n" + "k-e-y=\"quoted value\"\n\r" + " wrong= 'quotes' \n" + "odd= stripping # with comments "; + char s2[] = "this parser\n" + "\t\t\t# is\t\r" + " also\tused \r\n" + "for \"the conf\"\n" + "format\t !!"; + size_t pos = 0; + char *key, *value; + + assert_se(!line_get_key_value((char[]){ "" }, "=", &pos, &key, &value)); + assert_se(!line_get_key_value((char[]){ "\t" }, " \t", &pos, &key, &value)); + + pos = 0; + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "key")); + assert_se(streq8(value, "value")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "k-e-y")); + assert_se(streq8(value, "quoted value")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "wrong")); + assert_se(streq8(value, " 'quotes'")); + assert_se(line_get_key_value(s1, "=", &pos, &key, &value)); + assert_se(streq8(key, "odd")); + assert_se(streq8(value, " stripping # with comments")); + assert_se(!line_get_key_value(s1, "=", &pos, &key, &value)); + + pos = 0; + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "this")); + assert_se(streq8(value, "parser")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "also")); + assert_se(streq8(value, "used")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "for")); + assert_se(streq8(value, "the conf")); + assert_se(line_get_key_value(s2, " \t", &pos, &key, &value)); + assert_se(streq8(key, "format")); + assert_se(streq8(value, "!!")); + assert_se(!line_get_key_value(s2, " \t", &pos, &key, &value)); + + /* Let's make sure we don't fail on real os-release data. */ + _cleanup_free_ char *osrel = NULL; + if (read_full_file("/usr/lib/os-release", &osrel, NULL) >= 0) { + pos = 0; + while (line_get_key_value(osrel, "=", &pos, &key, &value)) { + assert_se(key); + assert_se(value); + printf("%s = %s\n", key, value); + } + } +} + +TEST(hexdump) { + char16_t *hex; + + hex = hexdump(NULL, 0); + assert(streq16(hex, u"")); + free(hex); + + hex = hexdump("1", 2); + assert(streq16(hex, u"3100")); + free(hex); + + hex = hexdump("abc", 4); + assert(streq16(hex, u"61626300")); + free(hex); + + hex = hexdump((uint8_t[]){ 0x0, 0x42, 0xFF, 0xF1, 0x1F }, 5); + assert(streq16(hex, u"0042fff11f")); + free(hex); +} + +_printf_(1, 2) static void test_printf_one(const char *format, ...) { + va_list ap, ap_efi; + va_start(ap, format); + va_copy(ap_efi, ap); + + _cleanup_free_ char *buf = NULL; + int r = vasprintf(&buf, format, ap); + assert_se(r >= 0); + log_info("/* %s(%s) -> \"%.100s\" */", __func__, format, buf); + + _cleanup_free_ char16_t *buf_efi = xvasprintf_status(0, format, ap_efi); + + bool eq = true; + for (size_t i = 0; i <= (size_t) r; i++) { + if (buf[i] != buf_efi[i]) + eq = false; + buf[i] = buf_efi[i]; + } + + log_info("%.100s", buf); + assert_se(eq); + + va_end(ap); + va_end(ap_efi); +} + +TEST(xvasprintf_status) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-zero-length" + test_printf_one(""); +#pragma GCC diagnostic pop + test_printf_one("string"); + test_printf_one("%%-%%%%"); + + test_printf_one("%p %p %32p %*p %*p", NULL, (void *) 0xF, &errno, 0, &saved_argc, 20, &saved_argv); + test_printf_one("%-10p %-32p %-*p %-*p", NULL, &errno, 0, &saved_argc, 20, &saved_argv); + + test_printf_one("%c %3c %*c %*c %-8c", '1', '!', 0, 'a', 9, '_', '>'); + + test_printf_one("%s %s %s", "012345", "6789", "ab"); + test_printf_one("%.4s %.4s %.4s %.0s", "cdefgh", "ijkl", "mn", "@"); + test_printf_one("%8s %8s %8s", "opqrst", "uvwx", "yz"); + test_printf_one("%8.4s %8.4s %8.4s %8.0s", "ABCDEF", "GHIJ", "KL", "$"); + test_printf_one("%4.8s %4.8s %4.8s", "ABCDEFGHIJ", "ABCDEFGH", "ABCD"); + + test_printf_one("%.*s %.*s %.*s %.*s", 4, "012345", 4, "6789", 4, "ab", 0, "&"); + test_printf_one("%*s %*s %*s", 8, "cdefgh", 8, "ijkl", 8, "mn"); + test_printf_one("%*.*s %*.*s %*.*s %*.*s", 8, 4, "opqrst", 8, 4, "uvwx", 8, 4, "yz", 8, 0, "#"); + test_printf_one("%*.*s %*.*s %*.*s", 3, 8, "OPQRST", 3, 8, "UVWX", 3, 8, "YZ"); + + test_printf_one("%ls %ls %ls", L"012345", L"6789", L"ab"); + test_printf_one("%.4ls %.4ls %.4ls %.0ls", L"cdefgh", L"ijkl", L"mn", L"@"); + test_printf_one("%8ls %8ls %8ls", L"opqrst", L"uvwx", L"yz"); + test_printf_one("%8.4ls %8.4ls %8.4ls %8.0ls", L"ABCDEF", L"GHIJ", L"KL", L"$"); + test_printf_one("%4.8ls %4.8ls %4.8ls", L"ABCDEFGHIJ", L"ABCDEFGH", L"ABCD"); + + test_printf_one("%.*ls %.*ls %.*ls %.*ls", 4, L"012345", 4, L"6789", 4, L"ab", 0, L"&"); + test_printf_one("%*ls %*ls %*ls", 8, L"cdefgh", 8, L"ijkl", 8, L"mn"); + test_printf_one("%*.*ls %*.*ls %*.*ls %*.*ls", 8, 4, L"opqrst", 8, 4, L"uvwx", 8, 4, L"yz", 8, 0, L"#"); + test_printf_one("%*.*ls %*.*ls %*.*ls", 3, 8, L"OPQRST", 3, 8, L"UVWX", 3, 8, L"YZ"); + + test_printf_one("%-14s %-10.0s %-10.3s", "left", "", "chopped"); + test_printf_one("%-14ls %-10.0ls %-10.3ls", L"left", L"", L"chopped"); + + test_printf_one("%.6s", (char[]){ 'n', 'o', ' ', 'n', 'u', 'l' }); + test_printf_one("%.6ls", (wchar_t[]){ 'n', 'o', ' ', 'n', 'u', 'l' }); + + test_printf_one("%u %u %u", 0U, 42U, 1234567890U); + test_printf_one("%i %i %i", 0, -42, -1234567890); + test_printf_one("%x %x %x", 0x0U, 0x42U, 0x123ABCU); + test_printf_one("%X %X %X", 0x1U, 0x43U, 0x234BCDU); + test_printf_one("%#x %#x %#x", 0x2U, 0x44U, 0x345CDEU); + test_printf_one("%#X %#X %#X", 0x3U, 0x45U, 0x456FEDU); + + test_printf_one("%u %lu %llu %zu", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%i %i %zi", INT_MIN, INT_MAX, SSIZE_MAX); + test_printf_one("%li %li %lli %lli", LONG_MIN, LONG_MAX, LLONG_MIN, LLONG_MAX); + test_printf_one("%x %#lx %llx %#zx", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%X %#lX %llX %#zX", UINT_MAX, ULONG_MAX, ULLONG_MAX, SIZE_MAX); + test_printf_one("%ju %ji %ji", UINTMAX_MAX, INTMAX_MIN, INTMAX_MAX); + test_printf_one("%ti %ti", PTRDIFF_MIN, PTRDIFF_MAX); + + test_printf_one("%" PRIu32 " %" PRIi32 " %" PRIi32, UINT32_MAX, INT32_MIN, INT32_MAX); + test_printf_one("%" PRIx32 " %" PRIX32, UINT32_MAX, UINT32_MAX); + test_printf_one("%#" PRIx32 " %#" PRIX32, UINT32_MAX, UINT32_MAX); + + test_printf_one("%" PRIu64 " %" PRIi64 " %" PRIi64, UINT64_MAX, INT64_MIN, INT64_MAX); + test_printf_one("%" PRIx64 " %" PRIX64, UINT64_MAX, UINT64_MAX); + test_printf_one("%#" PRIx64 " %#" PRIX64, UINT64_MAX, UINT64_MAX); + + test_printf_one("%.11u %.11i %.11x %.11X %#.11x %#.11X", 1U, -2, 3U, 0xA1U, 0xB2U, 0xC3U); + test_printf_one("%13u %13i %13x %13X %#13x %#13X", 4U, -5, 6U, 0xD4U, 0xE5U, 0xF6U); + test_printf_one("%9.5u %9.5i %9.5x %9.5X %#9.5x %#9.5X", 7U, -8, 9U, 0xA9U, 0xB8U, 0xC7U); + test_printf_one("%09u %09i %09x %09X %#09x %#09X", 4U, -5, 6U, 0xD6U, 0xE5U, 0xF4U); + + test_printf_one("%*u %.*u %*i %.*i", 15, 42U, 15, 43U, 15, -42, 15, -43); + test_printf_one("%*.*u %*.*i", 14, 10, 13U, 14, 10, -14); + test_printf_one("%*x %*X %.*x %.*X", 15, 0x1AU, 15, 0x2BU, 15, 0x3CU, 15, 0x4DU); + test_printf_one("%#*x %#*X %#.*x %#.*X", 15, 0xA1U, 15, 0xB2U, 15, 0xC3U, 15, 0xD4U); + test_printf_one("%*.*x %*.*X", 14, 10, 0x1AU, 14, 10, 0x2BU); + test_printf_one("%#*.*x %#*.*X", 14, 10, 0x3CU, 14, 10, 0x4DU); + + test_printf_one("%+.5i %+.5i % .7i % .7i", -15, 51, -15, 51); + test_printf_one("%+5.i %+5.i % 7.i % 7.i", -15, 51, -15, 51); + + test_printf_one("%-10u %-10i %-10x %#-10X %- 10i", 1u, -2, 0xA2D2u, 0XB3F4u, -512); + test_printf_one("%-10.6u %-10.6i %-10.6x %#-10.6X %- 10.6i", 1u, -2, 0xA2D2u, 0XB3F4u, -512); + test_printf_one("%-6.10u %-6.10i %-6.10x %#-6.10X %- 6.10i", 3u, -4, 0x2A2Du, 0X3B4Fu, -215); + test_printf_one("%*.u %.*i %.*i", -4, 9u, -4, 8, -4, -6); + + test_printf_one("%.0u %.0i %.0x %.0X", 0u, 0, 0u, 0u); + test_printf_one("%.*u %.*i %.*x %.*X", 0, 0u, 0, 0, 0, 0u, 0, 0u); + test_printf_one("%*u %*i %*x %*X", -1, 0u, -1, 0, -1, 0u, -1, 0u); + + test_printf_one("%*s%*s%*s", 256, "", 256, "", 4096, ""); /* Test buf growing. */ + test_printf_one("%0*i%0*i%0*i", 256, 0, 256, 0, 4096, 0); /* Test buf growing. */ + test_printf_one("%0*i", INT16_MAX, 0); /* Poor programmer's memzero. */ + + /* Non printf-compatible behavior tests below. */ + char16_t *s; + + assert_se(s = xasprintf_status(0, "\n \r \r\n")); + assert_se(streq16(s, u"\r\n \r \r\r\n")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_SUCCESS, "%m")); + assert_se(streq16(s, u"Success")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_SUCCESS, "%15m")); + assert_se(streq16(s, u" Success")); + s = mfree(s); + + assert_se(s = xasprintf_status(EFI_LOAD_ERROR, "%m")); + assert_se(streq16(s, u"Load error")); + s = mfree(s); + + assert_se(s = xasprintf_status(0x42, "%m")); + assert_se(streq16(s, u"0x42")); + s = mfree(s); +} + +TEST(efi_memchr) { + assert_se(streq8(efi_memchr("abcde", 'c', 5), "cde")); + assert_se(streq8(efi_memchr("abcde", 'c', 3), "cde")); + assert_se(streq8(efi_memchr("abcde", 'c', 2), NULL)); + assert_se(streq8(efi_memchr("abcde", 'c', 7), "cde")); + assert_se(streq8(efi_memchr("abcde", 'q', 5), NULL)); + assert_se(streq8(efi_memchr("abcde", 'q', 0), NULL)); + /* Test that the character is interpreted as unsigned char. */ + assert_se(streq8(efi_memchr("abcde", 'a', 6), efi_memchr("abcde", 'a' + 0x100, 6))); + assert_se(streq8(efi_memchr("abcde", 0, 6), "")); + assert_se(efi_memchr(NULL, 0, 0) == NULL); +} + +TEST(efi_memcmp) { + assert_se(efi_memcmp(NULL, NULL, 0) == 0); + assert_se(efi_memcmp(NULL, NULL, 1) == 0); + assert_se(efi_memcmp(NULL, "", 1) < 0); + assert_se(efi_memcmp("", NULL, 1) > 0); + assert_se(efi_memcmp("", "", 0) == 0); + assert_se(efi_memcmp("", "", 1) == 0); + assert_se(efi_memcmp("1", "1", 1) == 0); + assert_se(efi_memcmp("1", "2", 1) < 0); + assert_se(efi_memcmp("A", "a", 1) < 0); + assert_se(efi_memcmp("a", "A", 1) > 0); + assert_se(efi_memcmp("abc", "ab", 2) == 0); + assert_se(efi_memcmp("ab", "abc", 3) < 0); + assert_se(efi_memcmp("abc", "ab", 3) > 0); + assert_se(efi_memcmp("ab\000bd", "ab\000bd", 6) == 0); + assert_se(efi_memcmp("ab\000b\0", "ab\000bd", 6) < 0); +} + +TEST(efi_memcpy) { + char buf[10]; + + assert_se(!efi_memcpy(NULL, NULL, 0)); + assert_se(!efi_memcpy(NULL, "", 1)); + assert_se(efi_memcpy(buf, NULL, 0) == buf); + assert_se(efi_memcpy(buf, NULL, 1) == buf); + assert_se(efi_memcpy(buf, "a", 0) == buf); + + assert_se(efi_memcpy(buf, "", 1) == buf); + assert_se(memcmp(buf, "", 1) == 0); + assert_se(efi_memcpy(buf, "1", 1) == buf); + assert_se(memcmp(buf, "1", 1) == 0); + assert_se(efi_memcpy(buf, "23", 3) == buf); + assert_se(memcmp(buf, "23", 3) == 0); + assert_se(efi_memcpy(buf, "45\0ab\0\0\0c", 9) == buf); + assert_se(memcmp(buf, "45\0ab\0\0\0c", 9) == 0); +} + +TEST(efi_memset) { + char buf[10]; + + assert_se(!efi_memset(NULL, '1', 0)); + assert_se(!efi_memset(NULL, '1', 1)); + assert_se(efi_memset(buf, '1', 0) == buf); + + assert_se(efi_memset(buf, '2', 1) == buf); + assert_se(memcmp(buf, "2", 1) == 0); + assert_se(efi_memset(buf, '4', 4) == buf); + assert_se(memcmp(buf, "4444", 4) == 0); + assert_se(efi_memset(buf, 'a', 10) == buf); + assert_se(memcmp(buf, "aaaaaaaaaa", 10) == 0); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/boot/efi/ticks.c b/src/boot/efi/ticks.c new file mode 100644 index 0000000..873b9fe --- /dev/null +++ b/src/boot/efi/ticks.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ticks.h" +#include "util.h" +#include "vmm.h" + +#if defined(__i386__) || defined(__x86_64__) +# include + +static uint64_t ticks_read_arch(void) { + /* The TSC might or might not be virtualized in VMs (and thus might not be accurate or start at zero + * at boot), depending on hypervisor and CPU functionality. If it's not virtualized it's not useful + * for keeping time, hence don't attempt to use it. */ + if (in_hypervisor()) + return 0; + + return __builtin_ia32_rdtsc(); +} + +static uint64_t ticks_freq_arch(void) { + /* Detect TSC frequency from CPUID information if available. */ + + unsigned max_leaf, ebx, ecx, edx; + if (__get_cpuid(0, &max_leaf, &ebx, &ecx, &edx) == 0) + return 0; + + /* Leaf 0x15 is Intel only. */ + if (max_leaf < 0x15 || ebx != signature_INTEL_ebx || ecx != signature_INTEL_ecx || + edx != signature_INTEL_edx) + return 0; + + unsigned denominator, numerator, crystal_hz; + __cpuid(0x15, denominator, numerator, crystal_hz, edx); + if (denominator == 0 || numerator == 0) + return 0; + + uint64_t freq = crystal_hz; + if (crystal_hz == 0) { + /* If the crystal frequency is not available, try to deduce it from + * the processor frequency leaf if available. */ + if (max_leaf < 0x16) + return 0; + + unsigned core_mhz; + __cpuid(0x16, core_mhz, ebx, ecx, edx); + freq = core_mhz * 1000ULL * 1000ULL * denominator / numerator; + } + + return freq * numerator / denominator; +} + +#elif defined(__aarch64__) + +static uint64_t ticks_read_arch(void) { + uint64_t val; + asm volatile("mrs %0, cntvct_el0" : "=r"(val)); + return val; +} + +static uint64_t ticks_freq_arch(void) { + uint64_t freq; + asm volatile("mrs %0, cntfrq_el0" : "=r"(freq)); + return freq; +} + +#else + +static uint64_t ticks_read_arch(void) { + return 0; +} + +static uint64_t ticks_freq_arch(void) { + return 0; +} + +#endif + +static uint64_t ticks_freq(void) { + static uint64_t cache = 0; + + if (cache != 0) + return cache; + + cache = ticks_freq_arch(); + if (cache != 0) + return cache; + + /* As a fallback, count ticks during a millisecond delay. */ + uint64_t ticks_start = ticks_read_arch(); + BS->Stall(1000); + uint64_t ticks_end = ticks_read_arch(); + + if (ticks_end < ticks_start) /* Check for an overflow (which is not that unlikely, given on some + * archs the value is 32-bit) */ + return 0; + + cache = (ticks_end - ticks_start) * 1000UL; + return cache; +} + +uint64_t time_usec(void) { + uint64_t ticks = ticks_read_arch(); + if (ticks == 0) + return 0; + + uint64_t freq = ticks_freq(); + if (freq == 0) + return 0; + + return 1000UL * 1000UL * ticks / freq; +} diff --git a/src/boot/efi/ticks.h b/src/boot/efi/ticks.h new file mode 100644 index 0000000..fec3764 --- /dev/null +++ b/src/boot/efi/ticks.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +uint64_t time_usec(void); diff --git a/src/boot/efi/ubsan.c b/src/boot/efi/ubsan.c new file mode 100644 index 0000000..9512046 --- /dev/null +++ b/src/boot/efi/ubsan.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "log.h" + +typedef struct { + const char *filename; + uint32_t line; + uint32_t column; +} SourceLocation; + +/* Note that all ubsan handlers have a pointer to a type-specific struct passed as first argument. + * Since we do not inspect the extra data in it we can just treat it as a SourceLocation struct + * directly to keep things simple. */ + +#define HANDLER(name, ...) \ + _used_ _noreturn_ void __ubsan_handle_##name(__VA_ARGS__); \ + void __ubsan_handle_##name(__VA_ARGS__) { \ + log_error("systemd-boot: %s in %s@%u:%u", \ + __func__, \ + location->filename, \ + location->line, \ + location->column); \ + freeze(); \ + } + +#define UNARY_HANDLER(name) HANDLER(name, SourceLocation *location, uintptr_t v) +#define BINARY_HANDLER(name) HANDLER(name, SourceLocation *location, uintptr_t v1, uintptr_t v2) + +UNARY_HANDLER(load_invalid_value); +UNARY_HANDLER(negate_overflow); +UNARY_HANDLER(out_of_bounds); +UNARY_HANDLER(type_mismatch_v1); +UNARY_HANDLER(vla_bound_not_positive); + +BINARY_HANDLER(add_overflow); +BINARY_HANDLER(divrem_overflow); +BINARY_HANDLER(implicit_conversion); +BINARY_HANDLER(mul_overflow); +BINARY_HANDLER(pointer_overflow); +BINARY_HANDLER(shift_out_of_bounds); +BINARY_HANDLER(sub_overflow); + +HANDLER(builtin_unreachable, SourceLocation *location); +HANDLER(invalid_builtin, SourceLocation *location); +HANDLER(nonnull_arg, SourceLocation *location); +HANDLER(nonnull_return_v1, SourceLocation *attr_location, SourceLocation *location); diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c new file mode 100644 index 0000000..e56ccfd --- /dev/null +++ b/src/boot/efi/util.c @@ -0,0 +1,705 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "memory-util-fundamental.h" +#include "proto/device-path.h" +#include "proto/simple-text-io.h" +#include "ticks.h" +#include "util.h" +#include "version.h" + +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); + assert(buf || size == 0); + + flags |= EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS; + return RT->SetVariable((char16_t *) name, (EFI_GUID *) vendor, flags, size, (void *) buf); +} + +EFI_STATUS efivar_set(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags) { + assert(vendor); + assert(name); + + return efivar_set_raw(vendor, name, value, value ? strsize16(value) : 0, flags); +} + +EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t i, uint32_t flags) { + assert(vendor); + assert(name); + + _cleanup_free_ char16_t *str = xasprintf("%zu", i); + return efivar_set(vendor, name, str, flags); +} + +EFI_STATUS efivar_set_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t value, uint32_t flags) { + uint8_t buf[4]; + + assert(vendor); + assert(name); + + buf[0] = (uint8_t)(value >> 0U & 0xFF); + buf[1] = (uint8_t)(value >> 8U & 0xFF); + buf[2] = (uint8_t)(value >> 16U & 0xFF); + buf[3] = (uint8_t)(value >> 24U & 0xFF); + + return efivar_set_raw(vendor, name, buf, sizeof(buf), flags); +} + +EFI_STATUS efivar_set_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t value, uint32_t flags) { + uint8_t buf[8]; + + assert(vendor); + assert(name); + + buf[0] = (uint8_t)(value >> 0U & 0xFF); + buf[1] = (uint8_t)(value >> 8U & 0xFF); + buf[2] = (uint8_t)(value >> 16U & 0xFF); + buf[3] = (uint8_t)(value >> 24U & 0xFF); + buf[4] = (uint8_t)(value >> 32U & 0xFF); + buf[5] = (uint8_t)(value >> 40U & 0xFF); + buf[6] = (uint8_t)(value >> 48U & 0xFF); + buf[7] = (uint8_t)(value >> 56U & 0xFF); + + return efivar_set_raw(vendor, name, buf, sizeof(buf), flags); +} + +EFI_STATUS efivar_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags) { + EFI_STATUS err; + + assert(vendor); + assert(name); + + /* We could be wiping a non-volatile variable here and the spec makes no guarantees that won't incur + * in an extra write (and thus wear out). So check and clear only if needed. */ + err = efivar_get_raw(vendor, name, NULL, NULL); + if (err == EFI_SUCCESS) + return efivar_set_raw(vendor, name, NULL, 0, flags); + + return err; +} + +EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **ret) { + _cleanup_free_ char16_t *buf = NULL; + EFI_STATUS err; + char16_t *val; + size_t size; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, (char **) &buf, &size); + if (err != EFI_SUCCESS) + return err; + + /* Make sure there are no incomplete characters in the buffer */ + if ((size % sizeof(char16_t)) != 0) + return EFI_INVALID_PARAMETER; + + if (!ret) + return EFI_SUCCESS; + + /* Return buffer directly if it happens to be NUL terminated already */ + if (size >= sizeof(char16_t) && buf[size / sizeof(char16_t) - 1] == 0) { + *ret = TAKE_PTR(buf); + return EFI_SUCCESS; + } + + /* Make sure a terminating NUL is available at the end */ + val = xmalloc(size + sizeof(char16_t)); + + memcpy(val, buf, size); + val[size / sizeof(char16_t) - 1] = 0; /* NUL terminate */ + + *ret = val; + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t *ret) { + _cleanup_free_ char16_t *val = NULL; + EFI_STATUS err; + uint64_t u; + + assert(vendor); + assert(name); + + err = efivar_get(vendor, name, &val); + if (err != EFI_SUCCESS) + return err; + + if (!parse_number16(val, &u, NULL) || u > SIZE_MAX) + return EFI_INVALID_PARAMETER; + + if (ret) + *ret = u; + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret) { + _cleanup_free_ char *buf = NULL; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err != EFI_SUCCESS) + return err; + + if (size != sizeof(uint32_t)) + return EFI_BUFFER_TOO_SMALL; + + if (ret) + *ret = (uint32_t) buf[0] << 0U | (uint32_t) buf[1] << 8U | (uint32_t) buf[2] << 16U | + (uint32_t) buf[3] << 24U; + + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) { + _cleanup_free_ char *buf = NULL; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err != EFI_SUCCESS) + return err; + + if (size != sizeof(uint64_t)) + return EFI_BUFFER_TOO_SMALL; + + if (ret) + *ret = (uint64_t) buf[0] << 0U | (uint64_t) buf[1] << 8U | (uint64_t) buf[2] << 16U | + (uint64_t) buf[3] << 24U | (uint64_t) buf[4] << 32U | (uint64_t) buf[5] << 40U | + (uint64_t) buf[6] << 48U | (uint64_t) buf[7] << 56U; + + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **ret, size_t *ret_size) { + EFI_STATUS err; + + assert(vendor); + assert(name); + + size_t size = 0; + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, NULL); + if (err != EFI_BUFFER_TOO_SMALL) + return err; + + _cleanup_free_ void *buf = xmalloc(size); + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, buf); + if (err != EFI_SUCCESS) + return err; + + if (ret) + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; + + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret) { + _cleanup_free_ char *b = NULL; + size_t size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &b, &size); + if (err != EFI_SUCCESS) + return err; + + if (ret) + *ret = *b > 0; + + return EFI_SUCCESS; +} + +void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec) { + assert(vendor); + assert(name); + + if (usec == 0) + usec = time_usec(); + if (usec == 0) + return; + + _cleanup_free_ char16_t *str = xasprintf("%" PRIu64, usec); + efivar_set(vendor, name, str, 0); +} + +void convert_efi_path(char16_t *path) { + assert(path); + + for (size_t i = 0, fixed = 0;; i++) { + /* Fix device path node separator. */ + path[fixed] = (path[i] == '/') ? '\\' : path[i]; + + /* Double '\' is not allowed in EFI file paths. */ + if (fixed > 0 && path[fixed - 1] == '\\' && path[fixed] == '\\') + continue; + + if (path[i] == '\0') + break; + + fixed++; + } +} + +char16_t *xstr8_to_path(const char *str8) { + assert(str8); + char16_t *path = xstr8_to_16(str8); + convert_efi_path(path); + return path; +} + +static bool shall_be_whitespace(char16_t c) { + return c <= 0x20U || c == 0x7FU; /* All control characters + space */ +} + +char16_t* mangle_stub_cmdline(char16_t *cmdline) { + char16_t *p, *q, *e; + + if (!cmdline) + return cmdline; + + p = q = cmdline; + + /* Skip initial whitespace */ + while (shall_be_whitespace(*p)) + p++; + + /* Turn inner control characters into proper spaces */ + for (e = p; *p != 0; p++) { + if (shall_be_whitespace(*p)) { + *(q++) = ' '; + continue; + } + + *(q++) = *p; + e = q; /* remember last non-whitespace char */ + } + + /* Chop off trailing whitespace */ + *e = 0; + return cmdline; +} + +EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf) { + EFI_STATUS err; + + assert(file); + assert(size); + assert(buf); + + /* This is a drop-in replacement for EFI_FILE->Read() with the same API behavior. + * Some broken firmwares cannot handle large file reads and will instead return + * an error. As a workaround, read such files in small chunks. + * Note that we cannot just try reading the whole file first on such firmware as + * that will permanently break the handle even if it is re-opened. + * + * https://github.com/systemd/systemd/issues/25911 */ + + if (*size == 0) + return EFI_SUCCESS; + + size_t read = 0, remaining = *size; + while (remaining > 0) { + size_t chunk = MIN(1024U * 1024U, remaining); + + err = file->Read(file, &chunk, (uint8_t *) buf + read); + if (err != EFI_SUCCESS) + return err; + if (chunk == 0) + /* Caller requested more bytes than are in file. */ + break; + + assert(chunk <= remaining); + read += chunk; + remaining -= chunk; + } + + *size = read; + return EFI_SUCCESS; +} + +EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, size_t off, size_t size, char **ret, size_t *ret_size) { + _cleanup_(file_closep) EFI_FILE *handle = NULL; + _cleanup_free_ char *buf = NULL; + EFI_STATUS err; + + assert(dir); + assert(name); + assert(ret); + + err = dir->Open(dir, &handle, (char16_t*) name, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return err; + + if (size == 0) { + _cleanup_free_ EFI_FILE_INFO *info = NULL; + + err = get_file_info(handle, &info, NULL); + if (err != EFI_SUCCESS) + return err; + + size = info->FileSize; + } + + if (off > 0) { + err = handle->SetPosition(handle, off); + if (err != EFI_SUCCESS) + return err; + } + + /* Allocate some extra bytes to guarantee the result is NUL-terminated for char and char16_t strings. */ + size_t extra = size % sizeof(char16_t) + sizeof(char16_t); + + buf = xmalloc(size + extra); + err = chunked_read(handle, &size, buf); + if (err != EFI_SUCCESS) + return err; + + /* Note that chunked_read() changes size to reflect the actual bytes read. */ + memzero(buf + size, extra); + + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; + + return err; +} + +void print_at(size_t x, size_t y, size_t attr, const char16_t *str) { + assert(str); + ST->ConOut->SetCursorPosition(ST->ConOut, x, y); + ST->ConOut->SetAttribute(ST->ConOut, attr); + ST->ConOut->OutputString(ST->ConOut, (char16_t *) str); +} + +void clear_screen(size_t attr) { + log_wait(); + ST->ConOut->SetAttribute(ST->ConOut, attr); + ST->ConOut->ClearScreen(ST->ConOut); +} + +void sort_pointer_array( + void **array, + size_t n_members, + compare_pointer_func_t compare) { + + assert(array || n_members == 0); + assert(compare); + + if (n_members <= 1) + return; + + for (size_t i = 1; i < n_members; i++) { + size_t k; + void *entry = array[i]; + + for (k = i; k > 0; k--) { + if (compare(array[k - 1], entry) <= 0) + break; + + array[k] = array[k - 1]; + } + + array[k] = entry; + } +} + +EFI_STATUS get_file_info(EFI_FILE *handle, EFI_FILE_INFO **ret, size_t *ret_size) { + size_t size = EFI_FILE_INFO_MIN_SIZE; + _cleanup_free_ EFI_FILE_INFO *fi = NULL; + EFI_STATUS err; + + assert(handle); + assert(ret); + + fi = xmalloc(size); + err = handle->GetInfo(handle, MAKE_GUID_PTR(EFI_FILE_INFO), &size, fi); + if (err == EFI_BUFFER_TOO_SMALL) { + free(fi); + fi = xmalloc(size); /* GetInfo tells us the required size, let's use that now */ + err = handle->GetInfo(handle, MAKE_GUID_PTR(EFI_FILE_INFO), &size, fi); + } + + if (err != EFI_SUCCESS) + return err; + + *ret = TAKE_PTR(fi); + + if (ret_size) + *ret_size = size; + + return EFI_SUCCESS; +} + +EFI_STATUS readdir( + EFI_FILE *handle, + EFI_FILE_INFO **buffer, + size_t *buffer_size) { + + EFI_STATUS err; + size_t sz; + + assert(handle); + assert(buffer); + assert(buffer_size); + + /* buffer/buffer_size are both in and output parameters. Should be zero-initialized initially, and + * the specified buffer needs to be freed by caller, after final use. */ + + if (!*buffer) { + sz = EFI_FILE_INFO_MIN_SIZE; + *buffer = xmalloc(sz); + *buffer_size = sz; + } else + sz = *buffer_size; + + err = handle->Read(handle, &sz, *buffer); + if (err == EFI_BUFFER_TOO_SMALL) { + free(*buffer); + *buffer = xmalloc(sz); + *buffer_size = sz; + err = handle->Read(handle, &sz, *buffer); + } + if (err != EFI_SUCCESS) + return err; + + if (sz == 0) { + /* End of directory */ + free(*buffer); + *buffer = NULL; + *buffer_size = 0; + } + + return EFI_SUCCESS; +} + +bool is_ascii(const char16_t *f) { + if (!f) + return false; + + for (; *f != 0; f++) + if (*f > 127) + return false; + + return true; +} + +char16_t **strv_free(char16_t **v) { + if (!v) + return NULL; + + for (char16_t **i = v; *i; i++) + free(*i); + + free(v); + return NULL; +} + +EFI_STATUS open_directory( + EFI_FILE *root, + const char16_t *path, + EFI_FILE **ret) { + + _cleanup_(file_closep) EFI_FILE *dir = NULL; + _cleanup_free_ EFI_FILE_INFO *file_info = NULL; + EFI_STATUS err; + + assert(root); + + /* Opens a file, and then verifies it is actually a directory */ + + err = root->Open(root, &dir, (char16_t *) path, EFI_FILE_MODE_READ, 0); + if (err != EFI_SUCCESS) + return err; + + err = get_file_info(dir, &file_info, NULL); + if (err != EFI_SUCCESS) + return err; + if (!FLAGS_SET(file_info->Attribute, EFI_FILE_DIRECTORY)) + return EFI_LOAD_ERROR; + + *ret = TAKE_PTR(dir); + return EFI_SUCCESS; +} + +uint64_t get_os_indications_supported(void) { + uint64_t osind; + EFI_STATUS err; + + /* Returns the supported OS indications. If we can't acquire it, returns a zeroed out mask, i.e. no + * supported features. */ + + err = efivar_get_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndicationsSupported", &osind); + if (err != EFI_SUCCESS) + return 0; + + return osind; +} + +__attribute__((noinline)) void notify_debugger(const char *identity, volatile bool wait) { +#ifdef EFI_DEBUG + printf("%s@%p %s\n", identity, __executable_start, GIT_VERSION); + if (wait) + printf("Waiting for debugger to attach...\n"); + + /* This is a poor programmer's breakpoint to wait until a debugger + * has attached to us. Just "set variable wait = 0" or "return" to continue. */ + while (wait) + /* Prefer asm based stalling so that gdb has a source location to present. */ +# if defined(__i386__) || defined(__x86_64__) + asm volatile("pause"); +# elif defined(__aarch64__) + asm volatile("wfi"); +# else + BS->Stall(5000); +# endif +#endif +} + +#if defined(__i386__) || defined(__x86_64__) +static uint8_t inb(uint16_t port) { + uint8_t value; + asm volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static void outb(uint16_t port, uint8_t value) { + asm volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +void beep(unsigned beep_count) { + enum { + PITCH = 500, + BEEP_DURATION_USEC = 100 * 1000, + WAIT_DURATION_USEC = 400 * 1000, + + PIT_FREQUENCY = 0x1234dd, + SPEAKER_CONTROL_PORT = 0x61, + SPEAKER_ON_MASK = 0x03, + TIMER_PORT_MAGIC = 0xB6, + TIMER_CONTROL_PORT = 0x43, + TIMER_CONTROL2_PORT = 0x42, + }; + + /* Set frequency. */ + uint32_t counter = PIT_FREQUENCY / PITCH; + outb(TIMER_CONTROL_PORT, TIMER_PORT_MAGIC); + outb(TIMER_CONTROL2_PORT, counter & 0xFF); + outb(TIMER_CONTROL2_PORT, (counter >> 8) & 0xFF); + + uint8_t value = inb(SPEAKER_CONTROL_PORT); + + while (beep_count > 0) { + /* Turn speaker on. */ + value |= SPEAKER_ON_MASK; + outb(SPEAKER_CONTROL_PORT, value); + + BS->Stall(BEEP_DURATION_USEC); + + /* Turn speaker off. */ + value &= ~SPEAKER_ON_MASK; + outb(SPEAKER_CONTROL_PORT, value); + + beep_count--; + if (beep_count > 0) + BS->Stall(WAIT_DURATION_USEC); + } +} +#endif + +EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file) { + EFI_STATUS err; + EFI_FILE *file; + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *volume; + + assert(ret_file); + + err = BS->HandleProtocol(device, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), (void **) &volume); + if (err != EFI_SUCCESS) + return err; + + err = volume->OpenVolume(volume, &file); + if (err != EFI_SUCCESS) + return err; + + *ret_file = file; + return EFI_SUCCESS; +} + +void *find_configuration_table(const EFI_GUID *guid) { + for (size_t i = 0; i < ST->NumberOfTableEntries; i++) + if (efi_guid_equal(&ST->ConfigurationTable[i].VendorGuid, guid)) + return ST->ConfigurationTable[i].VendorTable; + + return NULL; +} + +static void remove_boot_count(char16_t *path) { + char16_t *prefix_end; + const char16_t *tail; + uint64_t ignored; + + assert(path); + + prefix_end = strchr16(path, '+'); + if (!prefix_end) + return; + + tail = prefix_end + 1; + + if (!parse_number16(tail, &ignored, &tail)) + return; + + if (*tail == '-') { + ++tail; + if (!parse_number16(tail, &ignored, &tail)) + return; + } + + if (!IN_SET(*tail, '\0', '.')) + return; + + strcpy16(prefix_end, tail); +} + +char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path) { + if (!file_path) + return NULL; + + /* A device path is allowed to have more than one file path node. If that is the case they are + * supposed to be concatenated. Unfortunately, the device path to text protocol simply converts the + * nodes individually and then combines those with the usual '/' for device path nodes. But this does + * not create a legal EFI file path that the file protocol can use. */ + + /* Make sure we really only got file paths. */ + for (const EFI_DEVICE_PATH *node = file_path; !device_path_is_end(node); + node = device_path_next_node(node)) + if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_FILEPATH_DP) + return NULL; + + _cleanup_free_ char16_t *file_path_str = NULL; + if (device_path_to_str(file_path, &file_path_str) != EFI_SUCCESS) + return NULL; + + convert_efi_path(file_path_str); + remove_boot_count(file_path_str); + return xasprintf("%ls.extra.d", file_path_str); +} + +void *xmalloc(size_t size) { + void *p = NULL; + assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS); + return p; +} diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h new file mode 100644 index 0000000..0306e32 --- /dev/null +++ b/src/boot/efi/util.h @@ -0,0 +1,213 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" +#include "log.h" +#include "proto/file-io.h" +#include "string-util-fundamental.h" + +/* This is provided by the linker. */ +extern uint8_t __executable_start[]; + +static inline void free(void *p) { + if (!p) + return; + + /* Debugging an invalid free requires trace logging to find the call site or a debugger attached. For + * release builds it is not worth the bother to even warn when we cannot even print a call stack. */ +#ifdef EFI_DEBUG + assert_se(BS->FreePool(p) == EFI_SUCCESS); +#else + (void) BS->FreePool(p); +#endif +} + +static inline void freep(void *p) { + free(*(void **) p); +} + +#define _cleanup_free_ _cleanup_(freep) + +_malloc_ _alloc_(1) _returns_nonnull_ _warn_unused_result_ +void *xmalloc(size_t size); + +_malloc_ _alloc_(1, 2) _returns_nonnull_ _warn_unused_result_ +static inline void *xmalloc_multiply(size_t n, size_t size) { + assert_se(!__builtin_mul_overflow(size, n, &size)); + return xmalloc(size); +} + +/* Use malloc attribute as this never returns p like userspace realloc. */ +_malloc_ _alloc_(3) _returns_nonnull_ _warn_unused_result_ +static inline void *xrealloc(void *p, size_t old_size, size_t new_size) { + void *t = xmalloc(new_size); + new_size = MIN(old_size, new_size); + if (new_size > 0) + memcpy(t, p, new_size); + free(p); + return t; +} + +_malloc_ _alloc_(2) _returns_nonnull_ _warn_unused_result_ +static inline void* xmemdup(const void *p, size_t l) { + return memcpy(xmalloc(l), p, l); +} + +#define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type))) + +typedef struct { + EFI_PHYSICAL_ADDRESS addr; + size_t n_pages; +} Pages; + +static inline void cleanup_pages(Pages *p) { + if (p->n_pages == 0) + return; +#ifdef EFI_DEBUG + assert_se(BS->FreePages(p->addr, p->n_pages) == EFI_SUCCESS); +#else + (void) BS->FreePages(p->addr, p->n_pages); +#endif +} + +#define _cleanup_pages_ _cleanup_(cleanup_pages) + +static inline Pages xmalloc_pages( + EFI_ALLOCATE_TYPE type, EFI_MEMORY_TYPE memory_type, size_t n_pages, EFI_PHYSICAL_ADDRESS addr) { + assert_se(BS->AllocatePages(type, memory_type, n_pages, &addr) == EFI_SUCCESS); + return (Pages) { + .addr = addr, + .n_pages = n_pages, + }; +} + +EFI_STATUS efivar_set(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags); +EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, size_t size, uint32_t flags); +EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t i, uint32_t flags); +EFI_STATUS efivar_set_uint32_le(const EFI_GUID *vendor, const char16_t *NAME, uint32_t value, uint32_t flags); +EFI_STATUS efivar_set_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t value, uint32_t flags); +void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec); + +EFI_STATUS efivar_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags); + +EFI_STATUS efivar_get(const EFI_GUID *vendor, const char16_t *name, char16_t **ret); +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **ret, size_t *ret_size); +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, size_t *ret); +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); + +void convert_efi_path(char16_t *path); +char16_t *xstr8_to_path(const char *stra); +char16_t *mangle_stub_cmdline(char16_t *cmdline); + +EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf); +EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, size_t off, size_t size, char **content, size_t *content_size); + +static inline void file_closep(EFI_FILE **handle) { + if (!*handle) + return; + + (*handle)->Close(*handle); +} + +static inline void unload_imagep(EFI_HANDLE *image) { + if (*image) + (void) BS->UnloadImage(*image); +} + +/* + * Allocated random UUID, intended to be shared across tools that implement + * the (ESP)\loader\entries\-.conf convention and the + * associated EFI variables. + */ +#define LOADER_GUID \ + { 0x4a67b082, 0x0a4c, 0x41cf, { 0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f } } + +/* Note that GUID is evaluated multiple times! */ +#define GUID_FORMAT_STR "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X" +#define GUID_FORMAT_VAL(g) (g).Data1, (g).Data2, (g).Data3, (g).Data4[0], (g).Data4[1], \ + (g).Data4[2], (g).Data4[3], (g).Data4[4], (g).Data4[5], (g).Data4[6], (g).Data4[7] + +void print_at(size_t x, size_t y, size_t attr, const char16_t *str); +void clear_screen(size_t attr); + +typedef int (*compare_pointer_func_t)(const void *a, const void *b); +void sort_pointer_array(void **array, size_t n_members, compare_pointer_func_t compare); + +EFI_STATUS get_file_info(EFI_FILE *handle, EFI_FILE_INFO **ret, size_t *ret_size); +EFI_STATUS readdir(EFI_FILE *handle, EFI_FILE_INFO **buffer, size_t *buffer_size); + +bool is_ascii(const char16_t *f); + +char16_t **strv_free(char16_t **l); + +static inline void strv_freep(char16_t ***p) { + strv_free(*p); +} + +EFI_STATUS open_directory(EFI_FILE *root_dir, const char16_t *path, EFI_FILE **ret); + +/* Conversion between EFI_PHYSICAL_ADDRESS and pointers is not obvious. The former is always 64-bit, even on + * 32-bit archs. And gcc complains if we cast a pointer to an integer of a different size. Hence let's do the + * conversion indirectly: first into uintptr_t and then extended to EFI_PHYSICAL_ADDRESS. */ +static inline EFI_PHYSICAL_ADDRESS POINTER_TO_PHYSICAL_ADDRESS(const void *p) { + return (EFI_PHYSICAL_ADDRESS) (uintptr_t) p; +} + +static inline void *PHYSICAL_ADDRESS_TO_POINTER(EFI_PHYSICAL_ADDRESS addr) { + /* On 32-bit systems the address might not be convertible (as pointers are 32-bit but + * EFI_PHYSICAL_ADDRESS 64-bit) */ + assert(addr <= UINTPTR_MAX); + return (void *) (uintptr_t) addr; +} + +uint64_t get_os_indications_supported(void); + +/* If EFI_DEBUG, print our name and version and also report the address of the image base so a debugger can + * be attached. See debug-sd-boot.sh for how this can be done. */ +void notify_debugger(const char *identity, bool wait); + +/* On x86 the compiler assumes a different incoming stack alignment than what we get. + * This will cause long long variables to be misaligned when building with + * '-mlong-double' (for correct struct layouts). Normally, the compiler realigns the + * stack itself on entry, but we have to do this ourselves here as the compiler does + * not know that this is our entry point. */ +#ifdef __i386__ +# define _realign_stack_ __attribute__((force_align_arg_pointer)) +#else +# define _realign_stack_ +#endif + +#define DEFINE_EFI_MAIN_FUNCTION(func, identity, wait_for_debugger) \ + EFI_SYSTEM_TABLE *ST; \ + EFI_BOOT_SERVICES *BS; \ + EFI_RUNTIME_SERVICES *RT; \ + _realign_stack_ \ + EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table); \ + EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { \ + ST = system_table; \ + BS = system_table->BootServices; \ + RT = system_table->RuntimeServices; \ + __stack_chk_guard_init(); \ + notify_debugger((identity), (wait_for_debugger)); \ + EFI_STATUS err = func(image); \ + log_wait(); \ + return err; \ + } + +#if defined(__i386__) || defined(__x86_64__) +void beep(unsigned beep_count); +#else +static inline void beep(unsigned beep_count) {} +#endif + +EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file); + +static inline bool efi_guid_equal(const EFI_GUID *a, const EFI_GUID *b) { + return memcmp(a, b, sizeof(EFI_GUID)) == 0; +} + +void *find_configuration_table(const EFI_GUID *guid); + +char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path); diff --git a/src/boot/efi/vmm.c b/src/boot/efi/vmm.c new file mode 100644 index 0000000..60e216d --- /dev/null +++ b/src/boot/efi/vmm.c @@ -0,0 +1,426 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if defined(__i386__) || defined(__x86_64__) +# include +#endif + +#include "confidential-virt-fundamental.h" +#include "device-path-util.h" +#include "drivers.h" +#include "efi-string.h" +#include "proto/device-path.h" +#include "string-util-fundamental.h" +#include "util.h" +#include "vmm.h" + +#define QEMU_KERNEL_LOADER_FS_MEDIA_GUID \ + { 0x1428f772, 0xb64a, 0x441e, { 0xb8, 0xc3, 0x9e, 0xbd, 0xd7, 0xf8, 0x93, 0xc7 } } + +#define VMM_BOOT_ORDER_GUID \ + { 0x668f4529, 0x63d0, 0x4bb5, { 0xb6, 0x5d, 0x6f, 0xbb, 0x9d, 0x36, 0xa4, 0x4a } } + +/* detect direct boot */ +bool is_direct_boot(EFI_HANDLE device) { + EFI_STATUS err; + VENDOR_DEVICE_PATH *dp; + + err = BS->HandleProtocol(device, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return false; + + /* 'qemu -kernel systemd-bootx64.efi' */ + if (dp->Header.Type == MEDIA_DEVICE_PATH && + dp->Header.SubType == MEDIA_VENDOR_DP && + memcmp(&dp->Guid, MAKE_GUID_PTR(QEMU_KERNEL_LOADER_FS_MEDIA), sizeof(EFI_GUID)) == 0) + return true; + + /* loaded from firmware volume (sd-boot added to ovmf) */ + if (dp->Header.Type == MEDIA_DEVICE_PATH && + dp->Header.SubType == MEDIA_PIWG_FW_VOL_DP) + return true; + + return false; +} + +/* + * Try find ESP when not loaded from ESP + * + * Inspect all filesystems known to the firmware, try find the ESP. In case VMMBootOrderNNNN variables are + * present they are used to inspect the filesystems in the specified order. When nothing was found or the + * variables are not present the function will do one final search pass over all filesystems. + * + * Recent OVMF builds store the qemu boot order (as specified using the bootindex property on the qemu + * command line) in VMMBootOrderNNNN. The variables contain a device path. + * + * Example qemu command line: + * qemu -virtio-scsi-pci,addr=14.0 -device scsi-cd,scsi-id=4,bootindex=1 + * + * Resulting variable: + * VMMBootOrder0000 = PciRoot(0x0)/Pci(0x14,0x0)/Scsi(0x4,0x0) + */ +EFI_STATUS vmm_open(EFI_HANDLE *ret_vmm_dev, EFI_FILE **ret_vmm_dir) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles; + EFI_STATUS err, dp_err; + + assert(ret_vmm_dev); + assert(ret_vmm_dir); + + /* Make sure all file systems have been initialized. Only do this in VMs as this is slow + * on some real firmwares. */ + (void) reconnect_all_drivers(); + + /* find all file system handles */ + err = BS->LocateHandleBuffer( + ByProtocol, MAKE_GUID_PTR(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL), NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return err; + + for (size_t order = 0;; order++) { + _cleanup_free_ EFI_DEVICE_PATH *dp = NULL; + + _cleanup_free_ char16_t *order_str = xasprintf("VMMBootOrder%04zx", order); + dp_err = efivar_get_raw(MAKE_GUID_PTR(VMM_BOOT_ORDER), order_str, (char **) &dp, NULL); + + for (size_t i = 0; i < n_handles; i++) { + _cleanup_(file_closep) EFI_FILE *root_dir = NULL, *efi_dir = NULL; + EFI_DEVICE_PATH *fs; + + err = BS->HandleProtocol( + handles[i], MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &fs); + if (err != EFI_SUCCESS) + return err; + + /* check against VMMBootOrderNNNN (if set) */ + if (dp_err == EFI_SUCCESS && !device_path_startswith(fs, dp)) + continue; + + err = open_volume(handles[i], &root_dir); + if (err != EFI_SUCCESS) + continue; + + /* simple ESP check */ + err = root_dir->Open(root_dir, &efi_dir, (char16_t*) u"\\EFI", + EFI_FILE_MODE_READ, + EFI_FILE_READ_ONLY | EFI_FILE_DIRECTORY); + if (err != EFI_SUCCESS) + continue; + + *ret_vmm_dev = handles[i]; + *ret_vmm_dir = TAKE_PTR(root_dir); + return EFI_SUCCESS; + } + + if (dp_err != EFI_SUCCESS) + return EFI_NOT_FOUND; + } + assert_not_reached(); +} + +static bool cpuid_in_hypervisor(void) { +#if defined(__i386__) || defined(__x86_64__) + unsigned eax, ebx, ecx, edx; + + /* This is a dumbed down version of src/basic/virt.c's detect_vm() that safely works in the UEFI + * environment. */ + + if (__get_cpuid(1, &eax, &ebx, &ecx, &edx) == 0) + return false; + + if (FLAGS_SET(ecx, 0x80000000U)) + return true; +#endif + + return false; +} + +#define SMBIOS_TABLE_GUID \ + GUID_DEF(0xeb9d2d31, 0x2d88, 0x11d3, 0x9a, 0x16, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d) +#define SMBIOS3_TABLE_GUID \ + GUID_DEF(0xf2fd1544, 0x9794, 0x4a2c, 0x99, 0x2e, 0xe5, 0xbb, 0xcf, 0x20, 0xe3, 0x94) + +typedef struct { + uint8_t anchor_string[4]; + uint8_t entry_point_structure_checksum; + uint8_t entry_point_length; + uint8_t major_version; + uint8_t minor_version; + uint16_t max_structure_size; + uint8_t entry_point_revision; + uint8_t formatted_area[5]; + uint8_t intermediate_anchor_string[5]; + uint8_t intermediate_checksum; + uint16_t table_length; + uint32_t table_address; + uint16_t number_of_smbios_structures; + uint8_t smbios_bcd_revision; +} _packed_ SmbiosEntryPoint; + +typedef struct { + uint8_t anchor_string[5]; + uint8_t entry_point_structure_checksum; + uint8_t entry_point_length; + uint8_t major_version; + uint8_t minor_version; + uint8_t docrev; + uint8_t entry_point_revision; + uint8_t reserved; + uint32_t table_maximum_size; + uint64_t table_address; +} _packed_ Smbios3EntryPoint; + +typedef struct { + uint8_t type; + uint8_t length; + uint8_t handle[2]; +} _packed_ SmbiosHeader; + +typedef struct { + SmbiosHeader header; + uint8_t vendor; + uint8_t bios_version; + uint16_t bios_segment; + uint8_t bios_release_date; + uint8_t bios_size; + uint64_t bios_characteristics; + uint8_t bios_characteristics_ext[2]; +} _packed_ SmbiosTableType0; + +typedef struct { + SmbiosHeader header; + uint8_t count; + char contents[]; +} _packed_ SmbiosTableType11; + +static const void *find_smbios_configuration_table(uint64_t *ret_size) { + assert(ret_size); + + const Smbios3EntryPoint *entry3 = find_configuration_table(MAKE_GUID_PTR(SMBIOS3_TABLE)); + if (entry3 && memcmp(entry3->anchor_string, "_SM3_", 5) == 0 && + entry3->entry_point_length <= sizeof(*entry3)) { + *ret_size = entry3->table_maximum_size; + return PHYSICAL_ADDRESS_TO_POINTER(entry3->table_address); + } + + const SmbiosEntryPoint *entry = find_configuration_table(MAKE_GUID_PTR(SMBIOS_TABLE)); + if (entry && memcmp(entry->anchor_string, "_SM_", 4) == 0 && + entry->entry_point_length <= sizeof(*entry)) { + *ret_size = entry->table_length; + return PHYSICAL_ADDRESS_TO_POINTER(entry->table_address); + } + + return NULL; +} + +static const SmbiosHeader *get_smbios_table(uint8_t type, uint64_t *ret_size_left) { + uint64_t size = 0; + const uint8_t *p = find_smbios_configuration_table(&size); + if (!p) + return NULL; + + for (;;) { + if (size < sizeof(SmbiosHeader)) + return NULL; + + const SmbiosHeader *header = (const SmbiosHeader *) p; + + /* End of table. */ + if (header->type == 127) + return NULL; + + if (size < header->length) + return NULL; + + if (header->type == type) { + if (ret_size_left) + *ret_size_left = size; + return header; /* Yay! */ + } + + /* Skip over formatted area. */ + size -= header->length; + p += header->length; + + /* Skip over string table. */ + for (;;) { + const uint8_t *e = memchr(p, 0, size); + if (!e) + return NULL; + + if (e == p) {/* Double NUL byte means we've reached the end of the string table. */ + p++; + size--; + break; + } + + size -= e + 1 - p; + p = e + 1; + } + } + + return NULL; +} + +static bool smbios_in_hypervisor(void) { + /* Look up BIOS Information (Type 0). */ + const SmbiosTableType0 *type0 = (const SmbiosTableType0 *) get_smbios_table(0, NULL); + if (!type0 || type0->header.length < sizeof(SmbiosTableType0)) + return false; + + /* Bit 4 of 2nd BIOS characteristics extension bytes indicates virtualization. */ + return FLAGS_SET(type0->bios_characteristics_ext[1], 1 << 4); +} + +bool in_hypervisor(void) { + static int cache = -1; + if (cache >= 0) + return cache; + + cache = cpuid_in_hypervisor() || smbios_in_hypervisor(); + return cache; +} + +const char* smbios_find_oem_string(const char *name) { + uint64_t left; + + assert(name); + + const SmbiosTableType11 *type11 = (const SmbiosTableType11 *) get_smbios_table(11, &left); + if (!type11 || type11->header.length < sizeof(SmbiosTableType11)) + return NULL; + + assert(left >= type11->header.length); + + const char *s = type11->contents; + left -= type11->header.length; + + for (const char *p = s; p < s + left; ) { + const char *e = memchr(p, 0, s + left - p); + if (!e || e == p) /* Double NUL byte means we've reached the end of the OEM strings. */ + break; + + const char *eq = startswith8(p, name); + if (eq && *eq == '=') + return eq + 1; + + p = e + 1; + } + + return NULL; +} + +#if defined(__i386__) || defined(__x86_64__) +static uint32_t cpuid_leaf(uint32_t eax, char ret_sig[static 13], bool swapped) { + /* zero-init as some queries explicitly require subleaf == 0 */ + uint32_t sig[3] = {}; + + if (swapped) + __cpuid_count(eax, 0, eax, sig[0], sig[2], sig[1]); + else + __cpuid_count(eax, 0, eax, sig[0], sig[1], sig[2]); + + memcpy(ret_sig, sig, sizeof(sig)); + ret_sig[12] = 0; /* \0-terminate the string to make string comparison possible */ + + return eax; +} + +static uint64_t msr(uint32_t index) { + uint64_t val; +#ifdef __x86_64__ + uint32_t low, high; + asm volatile ("rdmsr" : "=a"(low), "=d"(high) : "c"(index) : "memory"); + val = ((uint64_t)high << 32) | low; +#else + asm volatile ("rdmsr" : "=A"(val) : "c"(index) : "memory"); +#endif + return val; +} + +static bool detect_hyperv_sev(void) { + uint32_t eax, ebx, ecx, edx, feat; + char sig[13] = {}; + + feat = cpuid_leaf(CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS, sig, false); + + if (feat < CPUID_HYPERV_MIN || feat > CPUID_HYPERV_MAX) + return false; + + if (memcmp(sig, CPUID_SIG_HYPERV, sizeof(sig)) != 0) + return false; + + __cpuid(CPUID_HYPERV_FEATURES, eax, ebx, ecx, edx); + + if (ebx & CPUID_HYPERV_ISOLATION && !(ebx & CPUID_HYPERV_CPU_MANAGEMENT)) { + __cpuid(CPUID_HYPERV_ISOLATION_CONFIG, eax, ebx, ecx, edx); + + if ((ebx & CPUID_HYPERV_ISOLATION_TYPE_MASK) == CPUID_HYPERV_ISOLATION_TYPE_SNP) + return true; + } + + return false; +} + +static bool detect_sev(void) { + uint32_t eax, ebx, ecx, edx; + uint64_t msrval; + + __cpuid(CPUID_GET_HIGHEST_FUNCTION, eax, ebx, ecx, edx); + + if (eax < CPUID_AMD_GET_ENCRYPTED_MEMORY_CAPABILITIES) + return false; + + __cpuid(CPUID_AMD_GET_ENCRYPTED_MEMORY_CAPABILITIES, eax, ebx, ecx, edx); + + /* bit 1 == CPU supports SEV feature + * + * Note, Azure blocks this CPUID leaf from its SEV-SNP + * guests, so we must fallback to trying some HyperV + * specific CPUID checks. + */ + if (!(eax & EAX_SEV)) + return detect_hyperv_sev(); + + msrval = msr(MSR_AMD64_SEV); + + if (msrval & (MSR_SEV_SNP | MSR_SEV_ES | MSR_SEV)) + return true; + + return false; +} + +static bool detect_tdx(void) { + uint32_t eax, ebx, ecx, edx; + char sig[13] = {}; + + __cpuid(CPUID_GET_HIGHEST_FUNCTION, eax, ebx, ecx, edx); + + if (eax < CPUID_INTEL_TDX_ENUMERATION) + return false; + + cpuid_leaf(CPUID_INTEL_TDX_ENUMERATION, sig, true); + + if (memcmp(sig, CPUID_SIG_INTEL_TDX, sizeof(sig)) == 0) + return true; + + return false; +} +#endif /* ! __i386__ && ! __x86_64__ */ + +bool is_confidential_vm(void) { +#if defined(__i386__) || defined(__x86_64__) + char sig[13] = {}; + + if (!cpuid_in_hypervisor()) + return false; + + cpuid_leaf(0, sig, true); + + if (memcmp(sig, CPUID_SIG_AMD, sizeof(sig)) == 0) + return detect_sev(); + if (memcmp(sig, CPUID_SIG_INTEL, sizeof(sig)) == 0) + return detect_tdx(); +#endif /* ! __i386__ && ! __x86_64__ */ + + return false; +} diff --git a/src/boot/efi/vmm.h b/src/boot/efi/vmm.h new file mode 100644 index 0000000..df48af3 --- /dev/null +++ b/src/boot/efi/vmm.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +bool is_direct_boot(EFI_HANDLE device); +EFI_STATUS vmm_open(EFI_HANDLE *ret_qemu_dev, EFI_FILE **ret_qemu_dir); + +bool in_hypervisor(void); + +bool is_confidential_vm(void); + +const char* smbios_find_oem_string(const char *name); -- cgit v1.2.3