From 7be4b23649c02df33e4292f37ffc8aecf512955a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 23 Mar 2022 12:07:06 +0100 Subject: efi-loader: split efi-api.[ch] from efi-loader.[ch] Some refactoring: split efi-loader.[ch] in two: isolate the calls that implement out boot loader interface spec, and those which implement access to upstream UEFI firmware features. They are quite different in nature and behaviour, and even semantically it makes to keep these two separate. At the very least because the previous name "efi-loader.[ch]" suggests all was about loader-specific APIs, but much of it is generic uefi stuff... While we are at it, I renamed a bunch of return parameters to follow our usual ret_xyz naming. But besides renaming no real code changes. --- src/shared/efi-api.c | 556 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 src/shared/efi-api.c (limited to 'src/shared/efi-api.c') diff --git a/src/shared/efi-api.c b/src/shared/efi-api.c new file mode 100644 index 0000000000..b2c6af8754 --- /dev/null +++ b/src/shared/efi-api.c @@ -0,0 +1,556 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "dirent-util.h" +#include "efi-api.h" +#include "efivars.h" +#include "fd-util.h" +#include "sort-util.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "utf8.h" + +#if ENABLE_EFI + +#define LOAD_OPTION_ACTIVE 0x00000001 +#define MEDIA_DEVICE_PATH 0x04 +#define MEDIA_HARDDRIVE_DP 0x01 +#define MEDIA_FILEPATH_DP 0x04 +#define SIGNATURE_TYPE_GUID 0x02 +#define MBR_TYPE_EFI_PARTITION_TABLE_HEADER 0x02 +#define END_DEVICE_PATH_TYPE 0x7f +#define END_ENTIRE_DEVICE_PATH_SUBTYPE 0xff + +#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI UINT64_C(0x0000000000000001) + +#define boot_option__contents \ + { \ + uint32_t attr; \ + uint16_t path_len; \ + uint16_t title[]; \ + } + +struct boot_option boot_option__contents; +struct boot_option__packed boot_option__contents _packed_; +assert_cc(offsetof(struct boot_option, title) == offsetof(struct boot_option__packed, title)); +/* sizeof(struct boot_option) != sizeof(struct boot_option__packed), so + * the *size* of the structure should not be used anywhere below. */ + +struct drive_path { + uint32_t part_nr; + uint64_t part_start; + uint64_t part_size; + char signature[16]; + uint8_t mbr_type; + uint8_t signature_type; +} _packed_; + +#define device_path__contents \ + { \ + uint8_t type; \ + uint8_t sub_type; \ + uint16_t length; \ + union { \ + uint16_t path[0]; \ + struct drive_path drive; \ + }; \ + } + +struct device_path device_path__contents; +struct device_path__packed device_path__contents _packed_; +assert_cc(sizeof(struct device_path) == sizeof(struct device_path__packed)); + +int efi_reboot_to_firmware_supported(void) { + _cleanup_free_ void *v = NULL; + static int cache = -1; + uint64_t b; + size_t s; + int r; + + if (cache > 0) + return 0; + if (cache == 0) + return -EOPNOTSUPP; + + if (!is_efi_boot()) + goto not_supported; + + r = efi_get_variable(EFI_GLOBAL_VARIABLE(OsIndicationsSupported), NULL, &v, &s); + if (r == -ENOENT) + goto not_supported; /* variable doesn't exist? it's not supported then */ + if (r < 0) + return r; + if (s != sizeof(uint64_t)) + return -EINVAL; + + b = *(uint64_t*) v; + if (!(b & EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) + goto not_supported; /* bit unset? it's not supported then */ + + cache = 1; + return 0; + +not_supported: + cache = 0; + return -EOPNOTSUPP; +} + +static int get_os_indications(uint64_t *ret) { + static struct stat cache_stat = {}; + _cleanup_free_ void *v = NULL; + static uint64_t cache; + struct stat new_stat; + size_t s; + int r; + + assert(ret); + + /* Let's verify general support first */ + r = efi_reboot_to_firmware_supported(); + if (r < 0) + return r; + + /* stat() the EFI variable, to see if the mtime changed. If it did we need to cache again. */ + if (stat(EFIVAR_PATH(EFI_GLOBAL_VARIABLE(OsIndications)), &new_stat) < 0) { + if (errno != ENOENT) + return -errno; + + /* Doesn't exist? Then we can exit early (also see below) */ + *ret = 0; + return 0; + + } else if (stat_inode_unmodified(&new_stat, &cache_stat)) { + /* inode didn't change, we can return the cached value */ + *ret = cache; + return 0; + } + + r = efi_get_variable(EFI_GLOBAL_VARIABLE(OsIndications), NULL, &v, &s); + if (r == -ENOENT) { + /* Some firmware implementations that do support OsIndications and report that with + * OsIndicationsSupported will remove the OsIndications variable when it is unset. Let's + * pretend it's 0 then, to hide this implementation detail. Note that this call will return + * -ENOENT then only if the support for OsIndications is missing entirely, as determined by + * efi_reboot_to_firmware_supported() above. */ + *ret = 0; + return 0; + } + if (r < 0) + return r; + if (s != sizeof(uint64_t)) + return -EINVAL; + + cache_stat = new_stat; + *ret = cache = *(uint64_t *)v; + return 0; +} + +int efi_get_reboot_to_firmware(void) { + int r; + uint64_t b; + + r = get_os_indications(&b); + if (r < 0) + return r; + + return !!(b & EFI_OS_INDICATIONS_BOOT_TO_FW_UI); +} + +int efi_set_reboot_to_firmware(bool value) { + int r; + uint64_t b, b_new; + + r = get_os_indications(&b); + if (r < 0) + return r; + + b_new = UPDATE_FLAG(b, EFI_OS_INDICATIONS_BOOT_TO_FW_UI, value); + + /* Avoid writing to efi vars store if we can due to firmware bugs. */ + if (b != b_new) + return efi_set_variable(EFI_GLOBAL_VARIABLE(OsIndications), &b_new, sizeof(uint64_t)); + + return 0; +} + +static ssize_t utf16_size(const uint16_t *s, size_t buf_len_bytes) { + size_t l = 0; + + /* Returns the size of the string in bytes without the terminating two zero bytes */ + + if (buf_len_bytes % sizeof(uint16_t) != 0) + return -EINVAL; + + while (l < buf_len_bytes / sizeof(uint16_t)) { + if (s[l] == 0) + return (l + 1) * sizeof(uint16_t); + l++; + } + + return -EINVAL; /* The terminator was not found */ +} + +struct guid { + uint32_t u1; + uint16_t u2; + uint16_t u3; + uint8_t u4[8]; +} _packed_; + +static void efi_guid_to_id128(const void *guid, sd_id128_t *id128) { + uint32_t u1; + uint16_t u2, u3; + const struct guid *uuid = guid; + + memcpy(&u1, &uuid->u1, sizeof(uint32_t)); + id128->bytes[0] = (u1 >> 24) & 0xff; + id128->bytes[1] = (u1 >> 16) & 0xff; + id128->bytes[2] = (u1 >> 8) & 0xff; + id128->bytes[3] = u1 & 0xff; + memcpy(&u2, &uuid->u2, sizeof(uint16_t)); + id128->bytes[4] = (u2 >> 8) & 0xff; + id128->bytes[5] = u2 & 0xff; + memcpy(&u3, &uuid->u3, sizeof(uint16_t)); + id128->bytes[6] = (u3 >> 8) & 0xff; + id128->bytes[7] = u3 & 0xff; + memcpy(&id128->bytes[8], uuid->u4, sizeof(uuid->u4)); +} + +int efi_get_boot_option( + uint16_t id, + char **ret_title, + sd_id128_t *ret_part_uuid, + char **ret_path, + bool *ret_active) { + + char variable[STRLEN(EFI_GLOBAL_VARIABLE_STR("Boot")) + 4 + 1]; + _cleanup_free_ uint8_t *buf = NULL; + size_t l; + struct boot_option *header; + ssize_t title_size; + _cleanup_free_ char *s = NULL, *p = NULL; + sd_id128_t p_uuid = SD_ID128_NULL; + int r; + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + xsprintf(variable, EFI_GLOBAL_VARIABLE_STR("Boot%04X"), id); + r = efi_get_variable(variable, NULL, (void **)&buf, &l); + if (r < 0) + return r; + if (l < offsetof(struct boot_option, title)) + return -ENOENT; + + header = (struct boot_option *)buf; + title_size = utf16_size(header->title, l - offsetof(struct boot_option, title)); + if (title_size < 0) + return title_size; + + if (ret_title) { + s = utf16_to_utf8(header->title, title_size); + if (!s) + return -ENOMEM; + } + + if (header->path_len > 0) { + uint8_t *dbuf; + size_t dnext, doff; + + doff = offsetof(struct boot_option, title) + title_size; + dbuf = buf + doff; + if (header->path_len > l - doff) + return -EINVAL; + + dnext = 0; + while (dnext < header->path_len) { + struct device_path *dpath; + + dpath = (struct device_path *)(dbuf + dnext); + if (dpath->length < 4) + break; + + /* Type 0x7F – End of Hardware Device Path, Sub-Type 0xFF – End Entire Device Path */ + if (dpath->type == END_DEVICE_PATH_TYPE && dpath->sub_type == END_ENTIRE_DEVICE_PATH_SUBTYPE) + break; + + dnext += dpath->length; + + /* Type 0x04 – Media Device Path */ + if (dpath->type != MEDIA_DEVICE_PATH) + continue; + + /* Sub-Type 1 – Hard Drive */ + if (dpath->sub_type == MEDIA_HARDDRIVE_DP) { + /* 0x02 – GUID Partition Table */ + if (dpath->drive.mbr_type != MBR_TYPE_EFI_PARTITION_TABLE_HEADER) + continue; + + /* 0x02 – GUID signature */ + if (dpath->drive.signature_type != SIGNATURE_TYPE_GUID) + continue; + + if (ret_part_uuid) + efi_guid_to_id128(dpath->drive.signature, &p_uuid); + continue; + } + + /* Sub-Type 4 – File Path */ + if (dpath->sub_type == MEDIA_FILEPATH_DP && !p && ret_path) { + p = utf16_to_utf8(dpath->path, dpath->length-4); + if (!p) + return -ENOMEM; + + efi_tilt_backslashes(p); + continue; + } + } + } + + if (ret_title) + *ret_title = TAKE_PTR(s); + if (ret_part_uuid) + *ret_part_uuid = p_uuid; + if (ret_path) + *ret_path = TAKE_PTR(p); + if (ret_active) + *ret_active = header->attr & LOAD_OPTION_ACTIVE; + + return 0; +} + +static void to_utf16(uint16_t *dest, const char *src) { + int i; + + for (i = 0; src[i] != '\0'; i++) + dest[i] = src[i]; + dest[i] = '\0'; +} + +static void id128_to_efi_guid(sd_id128_t id, void *guid) { + struct guid uuid = { + .u1 = id.bytes[0] << 24 | id.bytes[1] << 16 | id.bytes[2] << 8 | id.bytes[3], + .u2 = id.bytes[4] << 8 | id.bytes[5], + .u3 = id.bytes[6] << 8 | id.bytes[7], + }; + memcpy(uuid.u4, id.bytes+8, sizeof(uuid.u4)); + memcpy(guid, &uuid, sizeof(uuid)); +} + +static uint16_t *tilt_slashes(uint16_t *s) { + for (uint16_t *p = s; *p; p++) + if (*p == '/') + *p = '\\'; + + return s; +} + +int efi_add_boot_option( + uint16_t id, + const char *title, + uint32_t part, + uint64_t pstart, + uint64_t psize, + sd_id128_t part_uuid, + const char *path) { + + size_t size, title_len, path_len; + _cleanup_free_ char *buf = NULL; + struct boot_option *option; + struct device_path *devicep; + char variable[STRLEN(EFI_GLOBAL_VARIABLE_STR("Boot")) + 4 + 1]; + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + title_len = (strlen(title)+1) * 2; + path_len = (strlen(path)+1) * 2; + + buf = malloc0(offsetof(struct boot_option, title) + title_len + + sizeof(struct drive_path) + + sizeof(struct device_path) + path_len); + if (!buf) + return -ENOMEM; + + /* header */ + option = (struct boot_option *)buf; + option->attr = LOAD_OPTION_ACTIVE; + option->path_len = offsetof(struct device_path, drive) + sizeof(struct drive_path) + + offsetof(struct device_path, path) + path_len + + offsetof(struct device_path, path); + to_utf16(option->title, title); + size = offsetof(struct boot_option, title) + title_len; + + /* partition info */ + devicep = (struct device_path *)(buf + size); + devicep->type = MEDIA_DEVICE_PATH; + devicep->sub_type = MEDIA_HARDDRIVE_DP; + devicep->length = offsetof(struct device_path, drive) + sizeof(struct drive_path); + memcpy(&devicep->drive.part_nr, &part, sizeof(uint32_t)); + memcpy(&devicep->drive.part_start, &pstart, sizeof(uint64_t)); + memcpy(&devicep->drive.part_size, &psize, sizeof(uint64_t)); + id128_to_efi_guid(part_uuid, devicep->drive.signature); + devicep->drive.mbr_type = MBR_TYPE_EFI_PARTITION_TABLE_HEADER; + devicep->drive.signature_type = SIGNATURE_TYPE_GUID; + size += devicep->length; + + /* path to loader */ + devicep = (struct device_path *)(buf + size); + devicep->type = MEDIA_DEVICE_PATH; + devicep->sub_type = MEDIA_FILEPATH_DP; + devicep->length = offsetof(struct device_path, path) + path_len; + to_utf16(devicep->path, path); + tilt_slashes(devicep->path); + size += devicep->length; + + /* end of path */ + devicep = (struct device_path *)(buf + size); + devicep->type = END_DEVICE_PATH_TYPE; + devicep->sub_type = END_ENTIRE_DEVICE_PATH_SUBTYPE; + devicep->length = offsetof(struct device_path, path); + size += devicep->length; + + xsprintf(variable, EFI_GLOBAL_VARIABLE_STR("Boot%04X"), id); + return efi_set_variable(variable, buf, size); +} + +int efi_remove_boot_option(uint16_t id) { + char variable[STRLEN(EFI_GLOBAL_VARIABLE_STR("Boot")) + 4 + 1]; + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + xsprintf(variable, EFI_GLOBAL_VARIABLE_STR("Boot%04X"), id); + return efi_set_variable(variable, NULL, 0); +} + +int efi_get_boot_order(uint16_t **ret_order) { + _cleanup_free_ void *buf = NULL; + size_t l; + int r; + + assert(ret_order); + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + r = efi_get_variable(EFI_GLOBAL_VARIABLE(BootOrder), NULL, &buf, &l); + if (r < 0) + return r; + + if (l <= 0) + return -ENOENT; + + if (l % sizeof(uint16_t) > 0 || + l / sizeof(uint16_t) > INT_MAX) + return -EINVAL; + + *ret_order = TAKE_PTR(buf); + return (int) (l / sizeof(uint16_t)); +} + +int efi_set_boot_order(const uint16_t *order, size_t n) { + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + return efi_set_variable(EFI_GLOBAL_VARIABLE(BootOrder), order, n * sizeof(uint16_t)); +} + +static int boot_id_hex(const char s[static 4]) { + int id = 0; + + assert(s); + + for (int i = 0; i < 4; i++) + if (s[i] >= '0' && s[i] <= '9') + id |= (s[i] - '0') << (3 - i) * 4; + else if (s[i] >= 'A' && s[i] <= 'F') + id |= (s[i] - 'A' + 10) << (3 - i) * 4; + else + return -EINVAL; + + return id; +} + +static int cmp_uint16(const uint16_t *a, const uint16_t *b) { + return CMP(*a, *b); +} + +int efi_get_boot_options(uint16_t **ret_options) { + _cleanup_closedir_ DIR *dir = NULL; + _cleanup_free_ uint16_t *list = NULL; + int count = 0; + + assert(ret_options); + + if (!is_efi_boot()) + return -EOPNOTSUPP; + + dir = opendir(EFIVAR_PATH(".")); + if (!dir) + return -errno; + + FOREACH_DIRENT(de, dir, return -errno) { + int id; + + if (strncmp(de->d_name, "Boot", 4) != 0) + continue; + + if (strlen(de->d_name) != 45) + continue; + + if (strcmp(de->d_name + 8, EFI_GLOBAL_VARIABLE_STR("")) != 0) /* generate variable suffix using macro */ + continue; + + id = boot_id_hex(de->d_name + 4); + if (id < 0) + continue; + + if (!GREEDY_REALLOC(list, count + 1)) + return -ENOMEM; + + list[count++] = id; + } + + typesafe_qsort(list, count, cmp_uint16); + + *ret_options = TAKE_PTR(list); + + return count; +} + +bool efi_has_tpm2(void) { + static int cache = -1; + + /* Returns whether the system has a TPM2 chip which is known to the EFI firmware. */ + + if (cache < 0) { + + /* First, check if we are on an EFI boot at all. */ + if (!is_efi_boot()) + cache = false; + else { + /* Then, check if the ACPI table "TPM2" exists, which is the TPM2 event log table, see: + * https://trustedcomputinggroup.org/wp-content/uploads/TCG_ACPIGeneralSpecification_v1.20_r8.pdf + * This table exists whenever the firmware is hooked up to TPM2. */ + cache = access("/sys/firmware/acpi/tables/TPM2", F_OK) >= 0; + if (!cache && errno != ENOENT) + log_debug_errno(errno, "Unable to test whether /sys/firmware/acpi/tables/TPM2 exists, assuming it doesn't: %m"); + } + } + + return cache; +} + +#endif + +char *efi_tilt_backslashes(char *s) { + for (char *p = s; *p; p++) + if (*p == '\\') + *p = '/'; + + return s; +} -- cgit v1.2.3