diff options
Diffstat (limited to 'src/boot/efi/bcd.c')
-rw-r--r-- | src/boot/efi/bcd.c | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/src/boot/efi/bcd.c b/src/boot/efi/bcd.c new file mode 100644 index 0000000..7200012 --- /dev/null +++ b/src/boot/efi/bcd.c @@ -0,0 +1,306 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdalign.h> + +#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 usecase 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 the 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; +} |