diff options
Diffstat (limited to '')
50 files changed, 10743 insertions, 0 deletions
diff --git a/src/boot/efi/assert.c b/src/boot/efi/assert.c new file mode 100644 index 0000000..bb16d2b --- /dev/null +++ b/src/boot/efi/assert.c @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "util.h" + +void efi_assert(const char *expr, const char *file, unsigned line, const char *function) { + log_error_stall(L"systemd-boot assertion '%a' failed at %a:%u, function %a(). Halting.", expr, file, line, function); + for (;;) + BS->Stall(60 * 1000 * 1000); +} 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; +} diff --git a/src/boot/efi/bcd.h b/src/boot/efi/bcd.h new file mode 100644 index 0000000..c27af55 --- /dev/null +++ b/src/boot/efi/bcd.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdint.h> +#include <uchar.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..d7b94bc --- /dev/null +++ b/src/boot/efi/boot.c @@ -0,0 +1,2778 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efigpt.h> +#include <efilib.h> + +#include "bcd.h" +#include "bootspec-fundamental.h" +#include "console.h" +#include "devicetree.h" +#include "disk.h" +#include "drivers.h" +#include "efivars-fundamental.h" +#include "graphics.h" +#include "initrd.h" +#include "linux.h" +#include "measure.h" +#include "pe.h" +#include "vmm.h" +#include "random-seed.h" +#include "secure-boot.h" +#include "shim.h" +#include "ticks.h" +#include "util.h" +#include "xbootldr.h" + +#ifndef GNU_EFI_USE_MS_ABI + /* We do not use uefi_call_wrapper() in systemd-boot. As such, we rely on the + * compiler to do the calling convention conversion for us. This is check is + * to make sure the -DGNU_EFI_USE_MS_ABI was passed to the comiler. */ + #error systemd-boot requires compilation with GNU_EFI_USE_MS_ABI defined. +#endif + +#define TEXT_ATTR_SWAP(c) EFI_TEXT_ATTR(((c) & 0b11110000) >> 4, (c) & 0b1111) + +/* Magic string for recognizing our own binaries */ +_used_ _section_(".sdmagic") static const char magic[] = + "#### LoaderInfo: systemd-boot " GIT_VERSION " ####"; + +/* Makes systemd-boot available from \EFI\Linux\ for testing purposes. */ +_used_ _section_(".osrel") static const char osrel[] = + "ID=systemd-boot\n" + "VERSION=\"" GIT_VERSION "\"\n" + "NAME=\"systemd-boot " GIT_VERSION "\"\n"; + +enum loader_type { + 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, +}; + +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; + enum loader_type type; + char16_t *loader; + char16_t *devicetree; + char16_t *options; + 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; +} ConfigEntry; + +typedef struct { + ConfigEntry **entries; + UINTN entry_count; + UINTN idx_default; + UINTN idx_default_efivar; + uint32_t timeout_sec; /* Actual timeout used (efi_main() override > efivar > config). */ + uint32_t timeout_sec_config; + uint32_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 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; + RandomSeedMode random_seed_mode; +} 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 ↔ … */ +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, +}; + +enum { + IDX_MAX = INT16_MAX, + IDX_INVALID, +}; + +static void cursor_left(UINTN *cursor, UINTN *first) { + assert(cursor); + assert(first); + + if ((*cursor) > 0) + (*cursor)--; + else if ((*first) > 0) + (*first)--; +} + +static void cursor_right( + UINTN *cursor, + UINTN *first, + UINTN x_max, + UINTN 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, + UINTN x_max, + UINTN y_pos) { + + _cleanup_free_ char16_t *line = NULL, *print = NULL; + UINTN size, len, first = 0, cursor = 0, clear = 0; + + 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; + UINTN j; + UINTN cursor_color = 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 = 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; + + UINTN k; + for (k = first + cursor; k < len && line[k] == ' '; k++) + clear++; + for (; k < len && line[k] != ' '; k++) + clear++; + + for (UINTN 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, CHAR_BACKSPACE): + /* 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 (UINTN 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 (UINTN 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, CHAR_LINEFEED): + case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, CHAR_CARRIAGE_RETURN): /* 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, CHAR_BACKSPACE): + if (len == 0) + continue; + if (first == 0 && cursor == 0) + continue; + for (UINTN 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 (UINTN 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 UINTN entry_lookup_key(Config *config, UINTN start, char16_t key) { + assert(config); + + if (key == 0) + return IDX_INVALID; + + /* select entry by number key */ + if (key >= '1' && key <= '9') { + UINTN i = key - '0'; + if (i > config->entry_count) + i = config->entry_count; + return i-1; + } + + /* find matching key in config entries */ + for (UINTN i = start; i < config->entry_count; i++) + if (config->entries[i]->key == key) + return i; + + for (UINTN i = 0; i < start; i++) + if (config->entries[i]->key == key) + return i; + + return IDX_INVALID; +} + +static char16_t *update_timeout_efivar(uint32_t *t, bool inc) { + assert(t); + + switch (*t) { + case TIMEOUT_MAX: + *t = inc ? TIMEOUT_MAX : (*t - 1); + break; + case TIMEOUT_UNSET: + *t = inc ? TIMEOUT_MENU_FORCE : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_FORCE: + *t = inc ? TIMEOUT_MENU_HIDDEN : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_HIDDEN: + *t = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + default: + *t += inc ? 1 : -1; + } + + switch (*t) { + case TIMEOUT_UNSET: + return xstrdup16(u"Menu timeout defined by configuration file."); + case TIMEOUT_MENU_FORCE: + return xstrdup16(u"Timeout disabled, menu will always be shown."); + case TIMEOUT_MENU_HIDDEN: + return xstrdup16(u"Menu disabled. Hold down key at bootup to show menu."); + default: + return xpool_print(L"Menu timeout set to %u s.", *t); + } +} + +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 *) L"─") == EFI_SUCCESS; + + return cache; +} + +static void ps_string(const char16_t *fmt, const void *value) { + assert(fmt); + if (value) + Print(fmt, value); +} + +static void ps_bool(const char16_t *fmt, bool value) { + assert(fmt); + Print(fmt, yes_no(value)); +} + +static bool ps_continue(void) { + if (unicode_supported()) + Print(L"\n─── Press any key to continue, ESC or q to quit. ───\n\n"); + else + Print(L"\n--- Press any key to continue, ESC or q to quit. ---\n\n"); + + 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_status(Config *config, char16_t *loaded_image_path) { + UINTN 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(LOADER_GUID, L"LoaderDevicePartUUID", &device_part_uuid); + + /* We employ some unusual indentation here for readability. */ + + ps_string(L" systemd-boot version: %a\n", GIT_VERSION); + ps_string(L" loaded image: %s\n", loaded_image_path); + ps_string(L" loader partition UUID: %s\n", device_part_uuid); + ps_string(L" architecture: %a\n", EFI_MACHINE_TYPE_NAME); + Print(L" UEFI specification: %u.%02u\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + ps_string(L" firmware vendor: %s\n", ST->FirmwareVendor); + Print(L" firmware version: %u.%02u\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + Print(L" OS indications: %lu\n", get_os_indications_supported()); + Print(L" secure boot: %s (%s)\n", yes_no(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)), secure_boot_mode_to_string(secure)); + ps_bool(L" shim: %s\n", shim_loaded()); + ps_bool(L" TPM: %s\n", tpm_present()); + Print(L" console mode: %d/%ld (%" PRIuN L"x%" PRIuN L" @%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; + + switch (config->timeout_sec_config) { + case TIMEOUT_UNSET: + break; + case TIMEOUT_MENU_FORCE: + Print(L" timeout (config): menu-force\n"); break; + case TIMEOUT_MENU_HIDDEN: + Print(L" timeout (config): menu-hidden\n"); break; + default: + Print(L" timeout (config): %u s\n", config->timeout_sec_config); + } + + switch (config->timeout_sec_efivar) { + case TIMEOUT_UNSET: + break; + case TIMEOUT_MENU_FORCE: + Print(L" timeout (EFI var): menu-force\n"); break; + case TIMEOUT_MENU_HIDDEN: + Print(L" timeout (EFI var): menu-hidden\n"); break; + default: + Print(L" timeout (EFI var): %u s\n", config->timeout_sec_efivar); + } + + ps_string(L" default (config): %s\n", config->entry_default_config); + ps_string(L" default (EFI var): %s\n", config->entry_default_efivar); + ps_string(L" default (one-shot): %s\n", config->entry_oneshot); + ps_string(L" saved entry: %s\n", config->entry_saved); + ps_bool(L" editor: %s\n", config->editor); + ps_bool(L" auto-entries: %s\n", config->auto_entries); + ps_bool(L" auto-firmware: %s\n", config->auto_firmware); + ps_bool(L" beep: %s\n", config->beep); + ps_bool(L" reboot-for-bitlocker: %s\n", config->reboot_for_bitlocker); + ps_string(L" random-seed-mode: %s\n", random_seed_modes_table[config->random_seed_mode]); + + switch (config->secure_boot_enroll) { + case ENROLL_OFF: + Print(L" secure-boot-enroll: off\n"); break; + case ENROLL_MANUAL: + Print(L" secure-boot-enroll: manual\n"); break; + case ENROLL_FORCE: + Print(L" secure-boot-enroll: force\n"); break; + default: + assert_not_reached(); + } + + switch (config->console_mode) { + case CONSOLE_MODE_AUTO: + Print(L" console-mode (config): %s\n", L"auto"); break; + case CONSOLE_MODE_KEEP: + Print(L" console-mode (config): %s\n", L"keep"); break; + case CONSOLE_MODE_FIRMWARE_MAX: + Print(L" console-mode (config): %s\n", L"max"); break; + default: + Print(L" console-mode (config): %ld\n", config->console_mode); break; + } + + /* EFI var console mode is always a concrete value or unset. */ + if (config->console_mode_efivar != CONSOLE_MODE_KEEP) + Print(L"console-mode (EFI var): %ld\n", config->console_mode_efivar); + + if (!ps_continue()) + return; + + for (UINTN i = 0; i < config->entry_count; i++) { + ConfigEntry *entry = config->entries[i]; + + _cleanup_free_ char16_t *dp = NULL; + if (entry->device) + (void) device_path_to_str(DevicePathFromHandle(entry->device), &dp); + + Print(L" config entry: %" PRIuN L"/%" PRIuN L"\n", i + 1, config->entry_count); + ps_string(L" id: %s\n", entry->id); + ps_string(L" title: %s\n", entry->title); + ps_string(L" title show: %s\n", streq16(entry->title, entry->title_show) ? NULL : entry->title_show); + ps_string(L" sort key: %s\n", entry->sort_key); + ps_string(L" version: %s\n", entry->version); + ps_string(L" machine-id: %s\n", entry->machine_id); + ps_string(L" device: %s\n", dp); + ps_string(L" loader: %s\n", entry->loader); + STRV_FOREACH(initrd, entry->initrd) + Print(L" initrd: %s\n", *initrd); + ps_string(L" devicetree: %s\n", entry->devicetree); + ps_string(L" options: %s\n", entry->options); + ps_bool(L" internal call: %s\n", !!entry->call); + + ps_bool(L"counting boots: %s\n", entry->tries_left >= 0); + if (entry->tries_left >= 0) { + Print(L" tries: %u left, %u done\n", entry->tries_left, entry->tries_done); + Print(L" current path: %s\\%s\n", entry->path, entry->current_name); + Print(L" next path: %s\\%s\n", entry->path, entry->next_name); + } + + if (!ps_continue()) + return; + } +} + +static EFI_STATUS reboot_into_firmware(void) { + uint64_t osind = 0; + EFI_STATUS err; + + if (!FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) + return log_error_status_stall(EFI_UNSUPPORTED, L"Reboot to firmware interface not supported."); + + (void) efivar_get_uint64_le(EFI_GLOBAL_GUID, L"OsIndications", &osind); + osind |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI; + + err = efivar_set_uint64_le(EFI_GLOBAL_GUID, L"OsIndications", osind, EFI_VARIABLE_NON_VOLATILE); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error setting OsIndications: %r", err); + + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} + +static bool menu_run( + Config *config, + ConfigEntry **chosen_entry, + char16_t *loaded_image_path) { + + assert(config); + assert(chosen_entry); + + EFI_STATUS err; + UINTN visible_max = 0; + UINTN idx_highlight = config->idx_default; + UINTN idx_highlight_prev = 0; + UINTN idx, idx_first = 0, idx_last = 0; + bool new_mode = true, clear = true; + bool refresh = true, highlight = false; + UINTN x_start = 0, y_start = 0, y_status = 0; + UINTN x_max, y_max; + _cleanup_(strv_freep) char16_t **lines = NULL; + _cleanup_free_ char16_t *clearline = NULL, *separator = NULL, *status = NULL; + uint32_t timeout_efivar_saved = config->timeout_sec_efivar; + uint32_t timeout_remain = config->timeout_sec == TIMEOUT_MENU_FORCE ? 0 : config->timeout_sec; + bool exit = false, run = true, firmware_setup = false; + int64_t console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar; + UINTN default_efivar_saved = config->idx_default_efivar; + + 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 */ + Print(L" "); + + 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_stall(L"Error switching console mode: %r", err); + } + + UINTN line_width = 0, entry_padding = 3; + while (!exit) { + 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->entry_count <= visible_max || idx_highlight <= visible_max / 2) + idx_first = 0; + else if (idx_highlight >= config->entry_count - (visible_max / 2)) + idx_first = config->entry_count - 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 (UINTN i = 0; i < config->entry_count; 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->entry_count < visible_max) + y_start = ((visible_max - config->entry_count) / 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->entry_count) + 1, y_max - 1); + + lines = strv_free(lines); + clearline = mfree(clearline); + separator = mfree(separator); + + /* menu entries title lines */ + lines = xnew(char16_t *, config->entry_count + 1); + + for (UINTN i = 0; i < config->entry_count; i++) { + UINTN 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 (UINTN 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->entry_count] = NULL; + + clearline = xnew(char16_t, x_max + 1); + separator = xnew(char16_t, x_max + 1); + for (UINTN 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 (UINTN i = idx_first; i <= idx_last && i < config->entry_count; 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() ? L" ►" : L"=>"); + } + 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() ? L" ►" : L"=>"); + if (idx_highlight == config->idx_default_efivar) + print_at(x_start, + y_start + idx_highlight - idx_first, + COLOR_HIGHLIGHT, + unicode_supported() ? L" ►" : L"=>"); + highlight = false; + } + + if (timeout_remain > 0) { + free(status); + status = xpool_print(L"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. */ + UINTN len = strnlen16(status, x_max - 1); + UINTN 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) { + exit = true; + break; + } + + /* update status */ + continue; + } + if (err != EFI_SUCCESS) { + exit = true; + break; + } + + timeout_remain = 0; + + /* clear status after keystroke */ + status = mfree(status); + + idx_highlight_prev = idx_highlight; + + if (firmware_setup) { + firmware_setup = false; + if (key == KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN)) + reboot_into_firmware(); + 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->entry_count-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->entry_count-1) { + refresh = true; + idx_highlight = config->entry_count-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->entry_count-1) + idx_highlight = config->entry_count-1; + break; + + case KEYPRESS(0, 0, CHAR_LINEFEED): + case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN): + case KEYPRESS(0, SCAN_F3, 0): /* EZpad Mini 4s firmware sends malformed events */ + case KEYPRESS(0, SCAN_F3, CHAR_CARRIAGE_RETURN): /* Teclast X98+ II firmware sends malformed events */ + case KEYPRESS(0, SCAN_RIGHT, 0): + exit = true; + 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 = xstrdup16(u"(d)efault (t/T)timeout (e)dit (r/R)resolution (p)rint (h)elp"); + break; + + case KEYPRESS(0, 0, 'Q'): + exit = true; + run = false; + 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->timeout_sec_efivar, false); + break; + + case KEYPRESS(0, 0, '+'): + case KEYPRESS(0, 0, 't'): + status = update_timeout_efivar(&config->timeout_sec_efivar, 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); + exit = line_edit(&config->entries[idx_highlight]->options, x_max - 2, y_status); + print_at(1, y_status, COLOR_NORMAL, clearline + 2); + break; + + case KEYPRESS(0, 0, 'v'): + status = xpool_print( + L"systemd-boot " GIT_VERSION L" (" EFI_MACHINE_TYPE_NAME L"), " + L"UEFI Specification %u.%02u, Vendor %s %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')): + clear = true; + break; + + case KEYPRESS(0, 0, 'r'): + err = console_set_mode(CONSOLE_MODE_NEXT); + if (err != EFI_SUCCESS) + status = xpool_print(L"Error changing console mode: %r", err); + else { + config->console_mode_efivar = ST->ConOut->Mode->Mode; + status = xpool_print(L"Console mode changed to %ld.", 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 = xpool_print(L"Error resetting console mode: %r", err); + else + status = xpool_print(L"Console mode reset to %s default.", + config->console_mode == CONSOLE_MODE_KEEP ? L"firmware" : L"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)) { + firmware_setup = true; + /* Let's make sure the user really wants to do this. */ + status = xpool_print(L"Press Enter to reboot into firmware interface."); + } else + status = xpool_print(L"Reboot into firmware interface not supported."); + 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; + } + + *chosen_entry = config->entries[idx_highlight]; + + /* Update EFI vars after we left the menu to reduce NVRAM writes. */ + + if (default_efivar_saved != config->idx_default_efivar) + efivar_set(LOADER_GUID, L"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_set(LOADER_GUID, L"LoaderConfigConsoleMode", NULL, EFI_VARIABLE_NON_VOLATILE); + else + efivar_set_uint_string(LOADER_GUID, L"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_set(LOADER_GUID, L"LoaderConfigTimeout", NULL, EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_FORCE: + efivar_set(LOADER_GUID, u"LoaderConfigTimeout", u"menu-force", EFI_VARIABLE_NON_VOLATILE); + break; + case TIMEOUT_MENU_HIDDEN: + efivar_set(LOADER_GUID, u"LoaderConfigTimeout", u"menu-hidden", EFI_VARIABLE_NON_VOLATILE); + break; + default: + efivar_set_uint_string(LOADER_GUID, L"LoaderConfigTimeout", + config->timeout_sec_efivar, EFI_VARIABLE_NON_VOLATILE); + } + } + + clear_screen(COLOR_NORMAL); + return run; +} + +static void config_add_entry(Config *config, ConfigEntry *entry) { + assert(config); + assert(entry); + + /* This is just for paranoia. */ + assert(config->entry_count < IDX_MAX); + + if ((config->entry_count & 15) == 0) { + config->entries = xrealloc( + config->entries, + sizeof(void *) * config->entry_count, + sizeof(void *) * (config->entry_count + 16)); + } + config->entries[config->entry_count++] = entry; +} + +static void config_entry_free(ConfigEntry *entry) { + if (!entry) + return; + + 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); + free(entry); +} + +static inline void config_entry_freep(ConfigEntry **entry) { + config_entry_free(*entry); +} + +static char *line_get_key_value( + char *content, + const char *sep, + UINTN *pos, + char **key_ret, + char **value_ret) { + + char *line, *value; + UINTN linelen; + + assert(content); + assert(sep); + assert(pos); + assert(key_ret); + assert(value_ret); + + for (;;) { + line = content + *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 (content[*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'; + } + + *key_ret = line; + *value_ret = value; + return line; + } +} + +static void config_defaults_load_from_file(Config *config, char *content) { + char *line; + UINTN pos = 0; + char *key, *value; + EFI_STATUS err; + + assert(config); + assert(content); + + while ((line = line_get_key_value(content, " \t", &pos, &key, &value))) { + if (streq8(key, "timeout")) { + 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_stall(L"Error parsing 'timeout' config option: %a", value); + continue; + } + config->timeout_sec_config = u; + } + config->timeout_sec = config->timeout_sec_config; + continue; + } + + if (streq8(key, "default")) { + if (value[0] == '@' && !strcaseeq8(value, "@saved")) { + log_error_stall(L"Unsupported special entry identifier: %a", value); + continue; + } + free(config->entry_default_config); + config->entry_default_config = xstr8_to_16(value); + continue; + } + + if (streq8(key, "editor")) { + err = parse_boolean(value, &config->editor); + if (err != EFI_SUCCESS) + log_error_stall(L"Error parsing 'editor' config option: %a", value); + continue; + } + + if (streq8(key, "auto-entries")) { + err = parse_boolean(value, &config->auto_entries); + if (err != EFI_SUCCESS) + log_error_stall(L"Error parsing 'auto-entries' config option: %a", value); + continue; + } + + if (streq8(key, "auto-firmware")) { + err = parse_boolean(value, &config->auto_firmware); + if (err != EFI_SUCCESS) + log_error_stall(L"Error parsing 'auto-firmware' config option: %a", value); + continue; + } + + if (streq8(key, "beep")) { + err = parse_boolean(value, &config->beep); + if (err != EFI_SUCCESS) + log_error_stall(L"Error parsing 'beep' config option: %a", value); + continue; + } + + if (streq8(key, "reboot-for-bitlocker")) { + err = parse_boolean(value, &config->reboot_for_bitlocker); + if (err != EFI_SUCCESS) + log_error_stall(L"Error parsing 'reboot-for-bitlocker' config option: %a", value); + } + + 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, "off")) + config->secure_boot_enroll = ENROLL_OFF; + else + log_error_stall(L"Error parsing 'secure-boot-enroll' config option: %a", value); + continue; + } + + 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_stall(L"Error parsing 'console-mode' config option: %a", value); + continue; + } + config->console_mode = u; + } + continue; + } + + if (streq8(key, "random-seed-mode")) { + if (streq8(value, "off")) + config->random_seed_mode = RANDOM_SEED_OFF; + else if (streq8(value, "with-system-token")) + config->random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN; + else if (streq8(value, "always")) + config->random_seed_mode = RANDOM_SEED_ALWAYS; + else { + bool on; + + err = parse_boolean(value, &on); + if (err != EFI_SUCCESS) { + log_error_stall(L"Error parsing 'random-seed-mode' config option: %a", value); + continue; + } + + config->random_seed_mode = on ? RANDOM_SEED_ALWAYS : RANDOM_SEED_OFF; + } + continue; + } + } +} + +static void config_entry_parse_tries( + ConfigEntry *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 = xpool_print( + L"%.*s%u-%u%s", + prefix_len, + file, + LESS_BY(tries_left, 1u), + MIN(tries_done + 1, (uint64_t) INT_MAX), + suffix); +} + +static EFI_STATUS config_entry_bump_counters(ConfigEntry *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; + UINTN 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) { + log_error_stall(L"Error opening entry root path: %r", err); + return err; + } + + old_path = xpool_print(L"%s\\%s", 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) { + log_error_stall(L"Error opening boot entry: %r", err); + return err; + } + + err = get_file_info_harder(handle, &file_info, &file_info_size); + if (err != EFI_SUCCESS) { + log_error_stall(L"Error getting boot entry file info: %r", err); + return err; + } + + /* And rename the file */ + strcpy16(file_info->FileName, entry->next_name); + err = handle->SetInfo(handle, &GenericFileInfo, file_info_size, file_info); + if (err != EFI_SUCCESS) { + log_error_stall(L"Failed to rename '%s' to '%s', ignoring: %r", old_path, entry->next_name, err); + return err; + } + + /* Flush everything to disk, just in case… */ + err = handle->Flush(handle); + if (err != EFI_SUCCESS) { + log_error_stall(L"Error flushing boot entry file info: %r", err); + return err; + } + + /* 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 = xpool_print(L"%s\\%s", entry->path, entry->next_name); + efivar_set(LOADER_GUID, L"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 config_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_(config_entry_freep) ConfigEntry *entry = NULL; + char *line; + UINTN 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(ConfigEntry, 1); + *entry = (ConfigEntry) { + .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); + continue; + } + + if (streq8(key, "sort-key")) { + free(entry->sort_key); + entry->sort_key = xstr8_to_16(value); + continue; + } + + if (streq8(key, "version")) { + free(entry->version); + entry->version = xstr8_to_16(value); + continue; + } + + if (streq8(key, "machine-id")) { + free(entry->machine_id); + entry->machine_id = xstr8_to_16(value); + continue; + } + + if (streq8(key, "linux")) { + free(entry->loader); + entry->type = LOADER_LINUX; + entry->loader = xstr8_to_path(value); + entry->key = 'l'; + continue; + } + + 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; + } + continue; + } + + 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; + } + continue; + } + + if (streq8(key, "devicetree")) { + free(entry->devicetree); + entry->devicetree = xstr8_to_path(value); + continue; + } + + 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; + continue; + } + + if (streq8(key, "options")) { + _cleanup_free_ char16_t *new = NULL; + + new = xstr8_to_16(value); + if (entry->options) { + char16_t *s = xpool_print(L"%s %s", entry->options, new); + free(entry->options); + entry->options = s; + } else + entry->options = TAKE_PTR(new); + + continue; + } + } + + 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); + + config_entry_parse_tries(entry, path, file, L".conf"); + TAKE_PTR(entry); +} + +static EFI_STATUS efivar_get_timeout(const char16_t *var, uint32_t *ret_value) { + _cleanup_free_ char16_t *value = NULL; + EFI_STATUS err; + + assert(var); + assert(ret_value); + + err = efivar_get(LOADER_GUID, var, &value); + if (err != EFI_SUCCESS) + return err; + + 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; + UINTN value = 0; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + assert(root_dir); + + *config = (Config) { + .editor = true, + .auto_entries = true, + .auto_firmware = true, + .reboot_for_bitlocker = false, + .secure_boot_enroll = ENROLL_MANUAL, + .random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN, + .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, L"\\loader\\loader.conf", 0, 0, &content, NULL); + if (err == EFI_SUCCESS) + 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_stall(u"Error reading LoaderConfigTimeout EFI variable: %r", err); + + err = efivar_get_timeout(u"LoaderConfigTimeoutOneShot", &config->timeout_sec); + if (err == EFI_SUCCESS) { + /* Unset variable now, after all it's "one shot". */ + (void) efivar_set(LOADER_GUID, L"LoaderConfigTimeoutOneShot", NULL, EFI_VARIABLE_NON_VOLATILE); + + config->force_menu = true; /* force the menu when this is set */ + } else if (err != EFI_NOT_FOUND) + log_error_stall(u"Error reading LoaderConfigTimeoutOneShot EFI variable: %r", err); + + err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigConsoleMode", &value); + if (err == EFI_SUCCESS) + config->console_mode_efivar = value; + + err = efivar_get(LOADER_GUID, L"LoaderEntryOneShot", &config->entry_oneshot); + if (err == EFI_SUCCESS) + /* Unset variable now, after all it's "one shot". */ + (void) efivar_set(LOADER_GUID, L"LoaderEntryOneShot", NULL, EFI_VARIABLE_NON_VOLATILE); + + (void) efivar_get(LOADER_GUID, L"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, L"@saved"); + config->use_saved_entry_efivar = streq16(config->entry_default_efivar, L"@saved"); + if (config->use_saved_entry || config->use_saved_entry_efivar) + (void) efivar_get(LOADER_GUID, L"LoaderEntryLastBooted", &config->entry_saved); +} + +static void config_load_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; + UINTN 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, L"\\loader\\entries", &entries_dir); + if (err != EFI_SUCCESS) + return; + + for (;;) { + _cleanup_free_ char *content = NULL; + + err = readdir_harder(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, L".conf")) + continue; + if (startswith(f->FileName, L"auto-")) + continue; + + err = file_read(entries_dir, f->FileName, 0, 0, &content, NULL); + if (err == EFI_SUCCESS) + config_entry_add_type1(config, device, root_dir, L"\\loader\\entries", f->FileName, content, loaded_image_path); + } +} + +static int config_entry_compare(const ConfigEntry *a, const ConfigEntry *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 UINTN config_entry_find(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 (UINTN i = 0; i < config->entry_count; i++) + if (efi_fnmatch(pattern, config->entries[i]->id)) + return i; + + return IDX_INVALID; +} + +static void config_default_entry_select(Config *config) { + UINTN i; + + assert(config); + + i = config_entry_find(config, config->entry_oneshot); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + i = config_entry_find(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_entry_find(config, config->entry_saved); + else + i = config_entry_find(config, config->entry_default_config); + if (i != IDX_INVALID) { + config->idx_default = i; + return; + } + + /* select the first suitable entry */ + for (i = 0; i < config->entry_count; i++) { + if (config->entries[i]->type == LOADER_AUTO || config->entries[i]->call) + continue; + 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(ConfigEntry **entries, bool *unique, UINTN entry_count) { + bool is_unique = true; + + assert(entries); + assert(unique); + + for (UINTN i = 0; i < entry_count; i++) + for (UINTN k = i + 1; k < entry_count; k++) { + if (!streq16(entries[i]->title_show, entries[k]->title_show)) + continue; + + is_unique = unique[i] = unique[k] = false; + } + + return is_unique; +} + +/* generate a unique title, avoiding non-distinguishable menu entries */ +static void config_title_generate(Config *config) { + assert(config); + + bool unique[config->entry_count]; + + /* set title */ + for (UINTN i = 0; i < config->entry_count; 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->entry_count)) + return; + + /* add version to non-unique titles */ + for (UINTN i = 0; i < config->entry_count; 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 = xpool_print(L"%s (%s)", t, config->entries[i]->version); + } + + if (entries_unique(config->entries, unique, config->entry_count)) + return; + + /* add machine-id to non-unique titles */ + for (UINTN i = 0; i < config->entry_count; 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 = xpool_print( + L"%s (%.*s)", + t, + strnlen16(config->entries[i]->machine_id, 8), + config->entries[i]->machine_id); + } + + if (entries_unique(config->entries, unique, config->entry_count)) + return; + + /* add file name to non-unique titles */ + for (UINTN i = 0; i < config->entry_count; i++) { + if (unique[i]) + continue; + + _cleanup_free_ char16_t *t = config->entries[i]->title_show; + config->entries[i]->title_show = xpool_print(L"%s (%s)", 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 + }; + UINTN 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(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, magic, sizeof(magic)) == 0; +} + +static ConfigEntry *config_entry_add_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 = L"\\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, L"\\EFI\\BOOT\\GRUB" EFI_MACHINE_TYPE_NAME L".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; + + ConfigEntry *entry = xnew(ConfigEntry, 1); + *entry = (ConfigEntry) { + .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_entry_add_osx(Config *config) { + EFI_STATUS err; + UINTN n_handles = 0; + _cleanup_free_ EFI_HANDLE *handles = NULL; + + assert(config); + + if (!config->auto_entries) + return; + + err = BS->LocateHandleBuffer(ByProtocol, &FileSystemProtocol, NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return; + + for (UINTN i = 0; i < n_handles; i++) { + _cleanup_(file_closep) EFI_FILE *root = NULL; + + if (open_volume(handles[i], &root) != EFI_SUCCESS) + continue; + + if (config_entry_add_loader_auto( + config, + handles[i], + root, + NULL, + L"auto-osx", + 'a', + L"macOS", + L"\\System\\Library\\CoreServices\\boot.efi")) + break; + } +} + +static EFI_STATUS boot_windows_bitlocker(void) { + _cleanup_free_ EFI_HANDLE *handles = NULL; + UINTN 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, &BlockIoProtocol, NULL, &n_handles, &handles); + if (err != EFI_SUCCESS) + return err; + + /* Look for BitLocker magic string on all block drives. */ + bool found = false; + for (UINTN i = 0; i < n_handles; i++) { + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->HandleProtocol(handles[i], &BlockIoProtocol, (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; + UINTN 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(EFI_GLOBAL_GUID, L"BootOrder", (char **) &boot_order, &boot_order_size); + if (err != EFI_SUCCESS || boot_order_size % sizeof(uint16_t) != 0) + return err; + + for (UINTN i = 0; i < boot_order_size / sizeof(uint16_t); i++) { + _cleanup_free_ char *buf = NULL; + char16_t name[sizeof(L"Boot0000")]; + UINTN buf_size; + + SPrint(name, sizeof(name), L"Boot%04x", (uint32_t) boot_order[i]); + err = efivar_get_raw(EFI_GLOBAL_GUID, 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. */ + UINTN offset = sizeof(uint32_t) + sizeof(uint16_t); + if (buf_size < offset + sizeof(char16_t)) + continue; + + if (streq16((char16_t *) (buf + offset), L"Windows Boot Manager")) { + err = efivar_set_raw( + EFI_GLOBAL_GUID, + L"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; +} + +static void config_entry_add_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; + UINTN len; + + assert(config); + assert(device); + assert(root_dir); + + if (!config->auto_entries) + return; + + /* Try to find a better title. */ + err = file_read(root_dir, L"\\EFI\\Microsoft\\Boot\\BCD", 0, 100*1024, &bcd, &len); + if (err == EFI_SUCCESS) + title = get_bcd_title((uint8_t *) bcd, len); + + ConfigEntry *e = config_entry_add_loader_auto(config, device, root_dir, NULL, + L"auto-windows", 'w', title ?: L"Windows Boot Manager", + L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi"); + + if (config->reboot_for_bitlocker) + e->call = boot_windows_bitlocker; +#endif +} + +static void config_entry_add_unified( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir) { + + _cleanup_(file_closep) EFI_FILE *linux_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *f = NULL; + UINTN 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, L"\\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; + UINTN offs[_SECTION_MAX] = {}; + UINTN szs[_SECTION_MAX] = {}; + char *line; + UINTN pos = 0; + char *key, *value; + + err = readdir_harder(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, L".efi")) + continue; + if (startswith(f->FileName, L"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); + continue; + } + + if (streq8(key, "IMAGE_ID")) { + free(os_image_id); + os_image_id = xstr8_to_16(value); + continue; + } + + if (streq8(key, "NAME")) { + free(os_name); + os_name = xstr8_to_16(value); + continue; + } + + if (streq8(key, "ID")) { + free(os_id); + os_id = xstr8_to_16(value); + continue; + } + + if (streq8(key, "IMAGE_VERSION")) { + free(os_image_version); + os_image_version = xstr8_to_16(value); + continue; + } + + if (streq8(key, "VERSION")) { + free(os_version); + os_version = xstr8_to_16(value); + continue; + } + + if (streq8(key, "VERSION_ID")) { + free(os_version_id); + os_version_id = xstr8_to_16(value); + continue; + } + + if (streq8(key, "BUILD_ID")) { + free(os_build_id); + os_build_id = xstr8_to_16(value); + continue; + } + } + + 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; + + ConfigEntry *entry = xnew(ConfigEntry, 1); + *entry = (ConfigEntry) { + .id = xstrdup16(f->FileName), + .type = LOADER_UNIFIED_LINUX, + .title = xstrdup16(good_name), + .version = xstrdup16(good_version), + .device = device, + .loader = xpool_print(L"\\EFI\\Linux\\%s", f->FileName), + .sort_key = xstrdup16(good_sort_key), + .key = 'l', + .tries_done = -1, + .tries_left = -1, + }; + + strtolower16(entry->id); + config_add_entry(config, entry); + config_entry_parse_tries(entry, L"\\EFI\\Linux", f->FileName, L".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); + } + } +} + +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 = xbootldr_open(device, &new_device, &root_dir); + if (err != EFI_SUCCESS) + return; + + config_entry_add_unified(config, new_device, root_dir); + config_load_entries(config, new_device, root_dir, NULL); +} + +static EFI_STATUS initrd_prepare( + EFI_FILE *root, + const ConfigEntry *entry, + char16_t **ret_options, + void **ret_initrd, + UINTN *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; + UINTN size = 0; + _cleanup_free_ uint8_t *initrd = NULL; + + STRV_FOREACH(i, entry->initrd) { + _cleanup_free_ char16_t *o = options; + if (o) + options = xpool_print(L"%s initrd=%s", o, *i); + else + options = xpool_print(L"initrd=%s", *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_harder(handle, &info, NULL); + if (err != EFI_SUCCESS) + return err; + + if (info->FileSize == 0) /* Automatically skip over empty files */ + continue; + + UINTN 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 = xpool_print(L"%s %s", 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 ConfigEntry *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_stall(err, L"Error opening root path: %r", err); + + err = make_file_device_path(entry->device, entry->loader, &path); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error making file device path: %r", err); + + UINTN 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_stall(err, L"Error preparing initrd: %r", err); + + err = shim_load_image(parent_image, path, &image); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error loading %s: %r", entry->loader, err); + + /* DTBs are loaded by the kernel before ExitBootServices, and they can be used to map and assign + * arbitrary memory ranges, so skip it 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_stall(err, L"Error loading %s: %r", entry->devicetree, err); + } + + _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; + err = initrd_register(initrd, initrd_size, &initrd_handle); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error registering initrd: %r", err); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol(image, &LoadedImageProtocol, (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error getting LoadedImageProtocol handle: %r", err); + + char16_t *options = options_initrd ?: 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(LOADER_GUID, L"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_stall(err, L"Error finding kernel compat entry address: %r", err); + } 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_stall(err, L"Failed to execute %s (%s): %r", entry->title_show, entry->loader, err); +} + +static void config_free(Config *config) { + assert(config); + for (UINTN i = 0; i < config->entry_count; i++) + config_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; + UINTN sz = 0; + char *p; + + assert(config); + + for (UINTN i = 0; i < config->entry_count; i++) + sz += strsize16(config->entries[i]->id); + + p = buffer = xmalloc(sz); + + for (UINTN i = 0; i < config->entry_count; 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(LOADER_GUID, L"LoaderEntries", buffer, sz, 0); +} + +static void save_selected_entry(const Config *config, const ConfigEntry *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(LOADER_GUID, L"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(LOADER_GUID, L"LoaderEntryLastBooted", entry->id, EFI_VARIABLE_NON_VOLATILE); + } else + /* Delete the non-volatile var if not needed. */ + (void) efivar_set(LOADER_GUID, L"LoaderEntryLastBooted", NULL, 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; + ConfigEntry *entry = NULL; + + err = readdir_harder(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(ConfigEntry, 1); + *entry = (ConfigEntry) { + .id = xpool_print(L"secure-boot-keys-%s", dirent->FileName), + .title = xpool_print(L"Enroll Secure Boot keys: %s", dirent->FileName), + .path = xpool_print(L"\\loader\\keys\\%s", dirent->FileName), + .type = LOADER_SECURE_BOOT_KEYS, + .tries_done = -1, + .tries_left = -1, + }; + config_add_entry(config, entry); + + if (config->secure_boot_enroll == 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); + } + + 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 | + 0; + + _cleanup_free_ char16_t *infostr = NULL, *typestr = NULL; + char16_t uuid[37]; + + assert(loaded_image); + + efivar_set_time_usec(LOADER_GUID, L"LoaderTimeInitUSec", init_usec); + efivar_set(LOADER_GUID, L"LoaderInfo", L"systemd-boot " GIT_VERSION, 0); + + infostr = xpool_print(L"%s %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(LOADER_GUID, L"LoaderFirmwareInfo", infostr, 0); + + typestr = xpool_print(L"UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(LOADER_GUID, L"LoaderFirmwareType", typestr, 0); + + (void) efivar_set_uint64_le(LOADER_GUID, L"LoaderFeatures", loader_features, 0); + + /* the filesystem path to this image, to prevent adding ourselves to the menu */ + efivar_set(LOADER_GUID, L"LoaderImageIdentifier", loaded_image_path, 0); + + /* export the device path this image is started from */ + if (disk_get_part_uuid(loaded_image->DeviceHandle, uuid) == EFI_SUCCESS) + efivar_set(LOADER_GUID, L"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_entry_add_unified(config, loaded_image->DeviceHandle, root_dir); + + /* scan /loader/entries/\*.conf files */ + config_load_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->entry_count, (compare_pointer_func_t) config_entry_compare); + + /* if we find some well-known loaders, add them to the end of the list */ + config_entry_add_osx(config); + config_entry_add_windows(config, loaded_image->DeviceHandle, root_dir); + config_entry_add_loader_auto(config, loaded_image->DeviceHandle, root_dir, NULL, + L"auto-efi-shell", 's', L"EFI Shell", L"\\shell" EFI_MACHINE_TYPE_NAME ".efi"); + config_entry_add_loader_auto(config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + L"auto-efi-default", '\0', L"EFI Default Loader", NULL); + + if (config->auto_firmware && FLAGS_SET(get_os_indications_supported(), EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) { + ConfigEntry *entry = xnew(ConfigEntry, 1); + *entry = (ConfigEntry) { + .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); + } + + /* find if secure boot signing keys exist and autoload them if necessary + otherwise creates 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->entry_count == 0) + return; + + config_write_entries_to_variable(config); + + config_title_generate(config); + + /* select entry by configured pattern or EFI LoaderDefaultEntry= variable */ + config_default_entry_select(config); +} + +EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { + 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; + + InitializeLib(image, sys_table); + init_usec = time_usec(); + debug_hook(L"systemd-boot"); + /* Uncomment the next line if you need to wait for debugger. */ + // debug_break(); + + err = BS->OpenProtocol(image, + &LoadedImageProtocol, + (void **)&loaded_image, + image, + NULL, + EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error getting a LoadedImageProtocol handle: %r", err); + + (void) device_path_to_str(loaded_image->FilePath, &loaded_image_path); + + export_variables(loaded_image, loaded_image_path, init_usec); + + /* The firmware may skip initializing some devices for the sake of a faster boot. This is especially + * true for fastboot enabled firmwares. But this means that things we use like input devices or the + * xbootldr partition may not be available yet. Reconnect all drivers should hopefully make the + * firmware initialize everything we need. */ + if (is_direct_boot(loaded_image->DeviceHandle)) + (void) reconnect_all_drivers(); + + err = open_volume(loaded_image->DeviceHandle, &root_dir); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Unable to open root directory: %r", err); + + (void) load_drivers(image, loaded_image, root_dir); + + config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir); + + if (config.entry_count == 0) { + log_error_stall(L"No loader found. Configuration files in \\loader\\entries\\*.conf are needed."); + goto out; + } + + /* select entry or show menu when key is pressed or timeout is set */ + if (config.force_menu || config.timeout_sec > 0) + menu = true; + else { + 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 config entries */ + UINTN idx = entry_lookup_key(&config, config.idx_default, KEYCHAR(key)); + if (idx != IDX_INVALID) + config.idx_default = idx; + else + menu = true; + } + } + + for (;;) { + ConfigEntry *entry; + + entry = config.entries[config.idx_default]; + if (menu) { + efivar_set_time_usec(LOADER_GUID, L"LoaderTimeMenuUSec", 0); + if (!menu_run(&config, &entry, loaded_image_path)) + break; + } + + /* 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); + 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) config_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, config.random_seed_mode); + + err = image_start(image, entry); + if (err != EFI_SUCCESS) + goto out; + + menu = true; + config.timeout_sec = 0; + } + err = EFI_SUCCESS; +out: + BS->CloseProtocol(image, &LoadedImageProtocol, image, NULL); + return err; +} diff --git a/src/boot/efi/console.c b/src/boot/efi/console.c new file mode 100644 index 0000000..14c0008 --- /dev/null +++ b/src/boot/efi/console.c @@ -0,0 +1,309 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "console.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 inline 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; + UINTN index; + EFI_STATUS err; + _cleanup_(event_closep) EFI_EVENT timer = NULL; + + assert(key); + + if (!checked) { + /* Get the *first* TextInputEx device.*/ + err = BS->LocateProtocol(&SimpleTextInputExProtocol, 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, &SimpleTextInputExProtocol, (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_stall(err, L"Error creating timer event: %r", err); + + EFI_EVENT events[] = { + timer, + conInEx ? conInEx->WaitForKeyEx : ST->ConIn->WaitForKey, + extraInEx ? extraInEx->WaitForKeyEx : NULL, + }; + UINTN 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_stall(err, L"Error arming timer event: %r", err); + + (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_stall(err, L"Error waiting for events: %r", err); + + /* 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 UINTN, 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); + + 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(&GraphicsOutputProtocol, 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; + UINTN 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(UINTN *x_max, UINTN *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..673a8ee --- /dev/null +++ b/src/boot/efi/console.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "missing_efi.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(UINTN *x_max, UINTN *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..79b5d43 --- /dev/null +++ b/src/boot/efi/cpio.c @@ -0,0 +1,568 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "measure.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 (UINTN 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, + UINTN contents_size, + const char *target_dir_prefix, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + UINTN *cpio_buffer_size) { + + UINTN 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 > 32bit 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 > UINTN_MAX - target_dir_prefix_size) + return EFI_OUT_OF_RESOURCES; + l += target_dir_prefix_size; + + fname_size = strlen16(fname); + if (l > UINTN_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 == UINTN_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + /* Align the contents to 4 byte size */ + q = ALIGN4(contents_size); + if (q == UINTN_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (l > UINTN_MAX - q) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + l += q; /* Add contents to header */ + + if (*cpio_buffer_size > UINTN_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, + UINTN *cpio_buffer_size) { + + UINTN 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 > UINTN_MAX - path_size) + return EFI_OUT_OF_RESOURCES; + l += path_size; + + /* Align the whole header to 4 byte size */ + l = ALIGN4(l); + if (l == UINTN_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (*cpio_buffer_size > UINTN_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, + UINTN *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, + UINTN *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; +} + +static EFI_STATUS measure_cpio( + void *buffer, + UINTN buffer_size, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + bool *ret_measured) { + + int measured = -1; + EFI_STATUS err; + + assert(buffer || buffer_size == 0); + assert(tpm_pcr || n_tpm_pcr == 0); + + for (UINTN i = 0; i < n_tpm_pcr; i++) { + bool m; + + if (tpm_pcr[i] == UINT32_MAX) /* Disabled */ + continue; + + err = tpm_log_event( + tpm_pcr[i], + POINTER_TO_PHYSICAL_ADDRESS(buffer), + buffer_size, + tpm_description, + &m); + if (err != EFI_SUCCESS) { + log_error_stall(L"Unable to add initrd TPM measurement for PCR %u (%s), ignoring: %r", tpm_pcr[i], tpm_description, err); + measured = false; + continue; + } + + if (measured != false) + measured = m; + } + + if (ret_measured) + *ret_measured = measured > 0; + + return EFI_SUCCESS; +} + +static char16_t *get_dropin_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; !IsDevicePathEnd(node); node = NextDevicePathNode(node)) + if (DevicePathType(node) != MEDIA_DEVICE_PATH || DevicePathSubType(node) != 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); + return xpool_print(u"%s.extra.d", file_path_str); +} + +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, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *ret_buffer_size, + bool *ret_measured) { + + _cleanup_(file_closep) EFI_FILE *root = NULL, *extra_dir = NULL; + UINTN 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(tpm_pcr || n_tpm_pcr == 0); + 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_stall( + err, L"Unable to open root directory: %r", err); + + if (!dropin_dir) + dropin_dir = rel_dropin_dir = get_dropin_dir(loaded_image->FilePath); + + 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_stall(err, L"Failed to open extra directory of loaded image: %r", err); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir_harder(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to read extra directory of loaded image: %r", err); + 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) { + UINTN m; + + /* We allocate 16 entries at a time, as a matter of optimization */ + if (n_items > (UINTN_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + 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_stall(err, L"Failed to pack cpio prefix: %r", err); + + for (UINTN i = 0; i < n_items; i++) { + _cleanup_free_ char *content = NULL; + UINTN 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_stall(err, L"Failed to read %s, ignoring: %r", items[i], err); + 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_stall(err, L"Failed to pack cpio file %s: %r", dirent->FileName, err); + } + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to pack cpio trailer: %r"); + + err = measure_cpio(buffer, buffer_size, tpm_pcr, n_tpm_pcr, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return err; + + *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 = n_tpm_pcr > 0; + + 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, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *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; + UINTN buffer_size = 0; + EFI_STATUS err; + + assert(data || data_size == 0); + assert(target_dir_prefix); + assert(target_filename); + assert(tpm_pcr || n_tpm_pcr == 0); + 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_stall(err, L"Failed to pack cpio prefix: %r", err); + + 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_stall(err, L"Failed to pack cpio file %s: %r", target_filename, err); + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to pack cpio trailer: %r"); + + err = measure_cpio(buffer, buffer_size, tpm_pcr, n_tpm_pcr, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return err; + + *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..beebef3 --- /dev/null +++ b/src/boot/efi/cpio.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <stdbool.h> +#include <uchar.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, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *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, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *ret_buffer_size, + bool *ret_measured); diff --git a/src/boot/efi/devicetree.c b/src/boot/efi/devicetree.c new file mode 100644 index 0000000..0312670 --- /dev/null +++ b/src/boot/efi/devicetree.c @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> + +#include "devicetree.h" +#include "missing_efi.h" +#include "util.h" + +#define FDT_V1_SIZE (7*4) + +static void *get_dtb_table(void) { + for (UINTN i = 0; i < ST->NumberOfTableEntries; i++) + if (memcmp(&EfiDtbTableGuid, &ST->ConfigurationTable[i].VendorGuid, sizeof(EfiDtbTableGuid)) == 0) + return ST->ConfigurationTable[i].VendorTable; + return NULL; +} + +static EFI_STATUS devicetree_allocate(struct devicetree_state *state, UINTN size) { + UINTN 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 UINTN devicetree_allocated(const struct devicetree_state *state) { + assert(state); + return state->pages * EFI_PAGE_SIZE; +} + +static EFI_STATUS devicetree_fixup(struct devicetree_state *state, UINTN len) { + EFI_DT_FIXUP_PROTOCOL *fixup; + UINTN size; + EFI_STATUS err; + + assert(state); + + err = BS->LocateProtocol(&EfiDtFixupProtocol, NULL, (void **) &fixup); + if (err != EFI_SUCCESS) + return log_error_status_stall(EFI_SUCCESS, + L"Could not locate device tree fixup protocol, skipping."); + + 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; + UINTN 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; + UINTN len; + EFI_STATUS err; + + assert(state); + assert(root_dir); + assert(name); + + state->orig = get_dtb_table(); + if (!state->orig) + return EFI_UNSUPPORTED; + + 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_harder(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(&EfiDtbTableGuid, PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +EFI_STATUS devicetree_install_from_memory(struct devicetree_state *state, + const void *dtb_buffer, UINTN dtb_length) { + + EFI_STATUS err; + + assert(state); + assert(dtb_buffer && dtb_length > 0); + + state->orig = get_dtb_table(); + if (!state->orig) + return EFI_UNSUPPORTED; + + 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(&EfiDtbTableGuid, PHYSICAL_ADDRESS_TO_POINTER(state->addr)); +} + +void devicetree_cleanup(struct devicetree_state *state) { + EFI_STATUS err; + + if (!state->pages) + return; + + err = BS->InstallConfigurationTable(&EfiDtbTableGuid, 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..d512cb5 --- /dev/null +++ b/src/boot/efi/devicetree.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <uchar.h> + +struct devicetree_state { + EFI_PHYSICAL_ADDRESS addr; + UINTN 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, UINTN dtb_length); +void devicetree_cleanup(struct devicetree_state *state); diff --git a/src/boot/efi/disk.c b/src/boot/efi/disk.c new file mode 100644 index 0000000..5246626 --- /dev/null +++ b/src/boot/efi/disk.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "disk.h" +#include "util.h" + +EFI_STATUS disk_get_part_uuid(EFI_HANDLE *handle, char16_t uuid[static 37]) { + EFI_STATUS err; + EFI_DEVICE_PATH *dp; + + /* export the device path this image is started from */ + + if (!handle) + return EFI_NOT_FOUND; + + err = BS->HandleProtocol(handle, &DevicePathProtocol, (void **) &dp); + if (err != EFI_SUCCESS) + return err; + + for (; !IsDevicePathEnd(dp); dp = NextDevicePathNode(dp)) { + if (DevicePathType(dp) != MEDIA_DEVICE_PATH) + continue; + if (DevicePathSubType(dp) != MEDIA_HARDDRIVE_DP) + continue; + + /* The HD device path may be misaligned. */ + HARDDRIVE_DEVICE_PATH hd; + memcpy(&hd, dp, MIN(sizeof(hd), (size_t) DevicePathNodeLength(dp))); + + if (hd.SignatureType != SIGNATURE_TYPE_GUID) + continue; + + GuidToString(uuid, (EFI_GUID *) &hd.Signature); + return EFI_SUCCESS; + } + + return EFI_NOT_FOUND; +} diff --git a/src/boot/efi/disk.h b/src/boot/efi/disk.h new file mode 100644 index 0000000..1a5a187 --- /dev/null +++ b/src/boot/efi/disk.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <uchar.h> + +EFI_STATUS disk_get_part_uuid(EFI_HANDLE *handle, char16_t uuid[static 37]); diff --git a/src/boot/efi/drivers.c b/src/boot/efi/drivers.c new file mode 100644 index 0000000..7f2057f --- /dev/null +++ b/src/boot/efi/drivers.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.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 = xpool_print(L"\\EFI\\systemd\\drivers\\%s", fname); + err = make_file_device_path(loaded_image->DeviceHandle, spath, &path); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error making file device path: %r", err); + + err = BS->LoadImage(false, parent_image, path, NULL, 0, &image); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to load image %s: %r", fname, err); + + err = BS->HandleProtocol(image, &LoadedImageProtocol, (void **)&loaded_image); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to find protocol in driver image %s: %r", fname, err); + + if (loaded_image->ImageCodeType != EfiBootServicesCode && + loaded_image->ImageCodeType != EfiRuntimeServicesCode) + return log_error_status_stall(EFI_INVALID_PARAMETER, L"Image %s 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_stall(L"Failed to start image %s: %r", fname, err); + 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_stall(err, L"Failed to get list of handles: %r", err); + + 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; + UINTN dirent_size = 0, n_succeeded = 0; + EFI_STATUS err; + + err = open_directory( + root_dir, + L"\\EFI\\systemd\\drivers", + &drivers_dir); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to open \\EFI\\systemd\\drivers: %r", err); + + for (;;) { + err = readdir_harder(drivers_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to read extra directory of loaded image: %r", err); + 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 L".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..4ad526e --- /dev/null +++ b/src/boot/efi/drivers.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.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..2ba1567 --- /dev/null +++ b/src/boot/efi/efi-string.c @@ -0,0 +1,460 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdbool.h> +#include <stdint.h> + +#include "efi-string.h" + +#ifdef SD_BOOT +# include "util.h" +#else +# include <stdlib.h> +# 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 NULL; \ + } + +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; +} + +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); + +#ifdef SD_BOOT +/* To provide the actual implementation for these we need to remove the redirection to the builtins. */ +# undef memcmp +# undef memcpy +# undef memset +#else +/* And for userspace unit testing we need to give them an efi_ prefix. */ +# define memcmp efi_memcmp +# define memcpy efi_memcpy +# define memset efi_memset +#endif + +_used_ 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; +} + +_used_ _weak_ void *memcpy(void * restrict dest, const void * restrict src, size_t n) { + if (!dest || !src || n == 0) + return dest; + +#ifdef 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; +} + +_used_ _weak_ void *memset(void *p, int c, size_t n) { + if (!p || n == 0) + return p; + +#ifdef 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..9b2a9ad --- /dev/null +++ b/src/boot/efi/efi-string.h @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <stdbool.h> +#include <stddef.h> +#include <uchar.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)); +} + +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); + +#ifdef SD_BOOT +/* 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 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. */ +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/fuzz-bcd.c b/src/boot/efi/fuzz-bcd.c new file mode 100644 index 0000000..297b71f --- /dev/null +++ b/src/boot/efi/fuzz-bcd.c @@ -0,0 +1,22 @@ +/* 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; + + 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-string.c b/src/boot/efi/fuzz-efi-string.c new file mode 100644 index 0000000..3c0f0f3 --- /dev/null +++ b/src/boot/efi/fuzz-efi-string.c @@ -0,0 +1,40 @@ +/* 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; + + 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..dc646bc --- /dev/null +++ b/src/boot/efi/graphics.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright © 2013 Intel Corporation + * Authored by Joonas Lahtinen <joonas.lahtinen@linux.intel.com> + */ + +#include <efi.h> +#include <efilib.h> + +#include "graphics.h" +#include "missing_efi.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; + BOOLEAN uga_exists; + BOOLEAN stdin_locked; + EFI_STATUS err; + + err = BS->LocateProtocol((EFI_GUID *) EFI_CONSOLE_CONTROL_GUID, 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; + + 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..9dadd39 --- /dev/null +++ b/src/boot/efi/graphics.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright © 2013 Intel Corporation + * Authored by Joonas Lahtinen <joonas.lahtinen@linux.intel.com> + */ +#pragma once + +#include <efi.h> +#include <stdbool.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..d994ef8 --- /dev/null +++ b/src/boot/efi/initrd.c @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "initrd.h" +#include "macro-fundamental.h" +#include "missing_efi.h" +#include "util.h" + +/* extend LoadFileProtocol */ +struct initrd_loader { + EFI_LOAD_FILE_PROTOCOL load_file; + const void *address; + UINTN 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), 0 } + }, + .Guid = LINUX_INITRD_MEDIA_GUID + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = { sizeof(efi_initrd_device_path.end), 0 } + } +}; + +static EFIAPI EFI_STATUS initrd_load_file( + EFI_LOAD_FILE_PROTOCOL *this, + EFI_DEVICE_PATH *file_path, + BOOLEAN boot_policy, + UINTN *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, + UINTN 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(&EfiLoadFile2Protocol, &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, + &DevicePathProtocol, &efi_initrd_device_path, + &EfiLoadFile2Protocol, 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->OpenProtocol( + initrd_handle, &EfiLoadFile2Protocol, (void **) &loader, + NULL, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (err != EFI_SUCCESS) + return err; + + /* close the handle */ + (void) BS->CloseProtocol(initrd_handle, &EfiLoadFile2Protocol, NULL, NULL); + + /* uninstall all protocols thus destroying the handle */ + err = BS->UninstallMultipleProtocolInterfaces( + initrd_handle, + &DevicePathProtocol, &efi_initrd_device_path, + &EfiLoadFile2Protocol, 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..d1478e3 --- /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, + UINTN 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..48801f9 --- /dev/null +++ b/src/boot/efi/linux.c @@ -0,0 +1,155 @@ +/* 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 <efi.h> +#include <efilib.h> + +#include "initrd.h" +#include "linux.h" +#include "pe.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), 0 }, + }, + .Guid = STUB_PAYLOAD_GUID, + }, + .end = { + .Type = END_DEVICE_PATH_TYPE, + .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, + .Length = { sizeof(payload_device_path.end), 0 }, + }, + }; + + /* 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_stall(err, u"Bad kernel image: %r", err); + + _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_stall(err, u"Error loading kernel image: %r", err); + + EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + err = BS->HandleProtocol(kernel_image, &LoadedImageProtocol, (void **) &loaded_image); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, u"Error getting kernel loaded image protocol: %r", err); + + 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_stall(err, u"Error registering initrd: %r", err); + + 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_stall(err, u"Error starting kernel image: %r", err); +} diff --git a/src/boot/efi/linux.h b/src/boot/efi/linux.h new file mode 100644 index 0000000..f0a6a37 --- /dev/null +++ b/src/boot/efi/linux.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <uchar.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..6a5e431 --- /dev/null +++ b/src/boot/efi/linux_x86.c @@ -0,0 +1,215 @@ +/* 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 <efi.h> +#include <efilib.h> + +#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; /* 32bit entry address. */ + + /* Old kernels needs this set, while newer ones seem to ignore this. Note that this gets truncated on + * above 4G boots, which is fine as long as we do not use the value to jump to kernel entry. */ + params->hdr.code32_start = kernel; + +#ifdef __x86_64__ + kernel += KERNEL_SECTOR_SIZE; /* 64bit entry address. */ +#endif + + kernel += params->hdr.handover_offset; /* 32/64bit EFI handover address. */ + + /* Note in EFI mixed mode this now points to the correct 32bit handover entry point, allowing a 64bit + * kernel to be booted from a 32bit 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_stall(EFI_UNSUPPORTED, u"Unsupported kernel image."); + if (image_params->hdr.version < SETUP_VERSION_2_11) + return log_error_status_stall(EFI_UNSUPPORTED, u"Kernel too old."); + if (!image_params->hdr.relocatable_kernel) + return log_error_status_stall(EFI_UNSUPPORTED, u"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_stall(EFI_UNSUPPORTED, u"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); + + if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + linux_length > UINT32_MAX) + return log_error_status_stall( + EFI_UNSUPPORTED, + u"Unified kernel image was loaded above 4G, but kernel lacks support."); + if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) + initrd_length > UINT32_MAX) + return log_error_status_stall( + EFI_UNSUPPORTED, u"Initrd is above 4G, but kernel lacks support."); + + _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; + + linux_efi_handover(parent, (uintptr_t) linux_buffer, boot_params); + return EFI_LOAD_ERROR; +} diff --git a/src/boot/efi/measure.c b/src/boot/efi/measure.c new file mode 100644 index 0000000..0b5b626 --- /dev/null +++ b/src/boot/efi/measure.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if ENABLE_TPM + +#include <efi.h> +#include <efilib.h> + +#include "tpm-pcr.h" +#include "macro-fundamental.h" +#include "measure.h" +#include "missing_efi.h" +#include "util.h" + +static EFI_STATUS tpm1_measure_to_pcr_and_event_log( + const EFI_TCG *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + UINTN 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; + UINTN desc_len; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + tcg_event = xmalloc(offsetof(TCG_PCR_EVENT, Event) + desc_len); + memset(tcg_event, 0, 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 *) tcg, + buffer, buffer_size, + TCG_ALG_SHA, + tcg_event, + &event_number, + &event_log_last); +} + +static EFI_STATUS tpm2_measure_to_pcr_and_event_log( + EFI_TCG2 *tcg, + uint32_t pcrindex, + EFI_PHYSICAL_ADDRESS buffer, + uint64_t buffer_size, + const char16_t *description) { + + _cleanup_free_ EFI_TCG2_EVENT *tcg_event = NULL; + UINTN desc_len; + + assert(tcg); + assert(description); + + desc_len = strsize16(description); + tcg_event = xmalloc(offsetof(EFI_TCG2_EVENT, Event) + desc_len); + memset(tcg_event, 0, 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 *tcg1_interface_check(void) { + EFI_PHYSICAL_ADDRESS event_log_location, event_log_last_entry; + TCG_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + uint32_t features; + EFI_TCG *tcg; + + err = BS->LocateProtocol((EFI_GUID *) EFI_TCG_GUID, 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 * tcg2_interface_check(void) { + EFI_TCG2_BOOT_SERVICE_CAPABILITY capability = { + .Size = sizeof(capability), + }; + EFI_STATUS err; + EFI_TCG2 *tcg; + + err = BS->LocateProtocol((EFI_GUID *) EFI_TCG2_GUID, 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) { + TCG_BOOT_SERVICE_CAPABILITY *caps_1_0 = + (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, UINTN buffer_size, const char16_t *description, bool *ret_measured) { + EFI_TCG2 *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 *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_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN 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) { + int measured = -1; + EFI_STATUS err; + + /* Measures a load options string into the TPM2, i.e. the kernel command line */ + + for (UINTN i = 0; i < 2; i++) { + uint32_t pcr = i == 0 ? TPM_PCR_INDEX_KERNEL_PARAMETERS : TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT; + bool m; + + if (pcr == UINT32_MAX) /* Skip this one, if it's invalid, so that our 'measured' return value is not corrupted by it */ + continue; + + err = tpm_log_event(pcr, POINTER_TO_PHYSICAL_ADDRESS(load_options), strsize16(load_options), load_options, &m); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Unable to add load options (i.e. kernel command) line measurement to PCR %u: %r", pcr, err); + + measured = measured < 0 ? m : (measured && m); + } + + if (ret_measured) + *ret_measured = measured < 0 ? false : 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..19a50f4 --- /dev/null +++ b/src/boot/efi/measure.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <stdbool.h> +#include <uchar.h> + +#if ENABLE_TPM + +bool tpm_present(void); +EFI_STATUS tpm_log_event(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char16_t *description, bool *ret_measured); +EFI_STATUS tpm_log_event_ascii(uint32_t pcrindex, EFI_PHYSICAL_ADDRESS buffer, UINTN buffer_size, const char *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, UINTN 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, UINTN buffer_size, const char *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..0196314 --- /dev/null +++ b/src/boot/efi/meson.build @@ -0,0 +1,500 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +conf.set10('ENABLE_EFI', get_option('efi')) +conf.set10('HAVE_GNU_EFI', false) + +efi_config_h_dir = meson.current_build_dir() + +if not get_option('efi') or get_option('gnu-efi') == 'false' + if get_option('gnu-efi') == 'true' + error('gnu-efi support requested, but general efi support is disabled') + endif + subdir_done() +endif + +efi_arch = host_machine.cpu_family() +if efi_arch == 'x86' and '-m64' in get_option('efi-cflags') + efi_arch = 'x86_64' +elif efi_arch == 'x86_64' and '-m32' in get_option('efi-cflags') + efi_arch = 'x86' +endif +efi_arch = { + # host_cc_arch: [efi_arch (see Table 3-2 in UEFI spec), obsolete gnu_efi_inc_arch] + 'x86': ['ia32', 'ia32'], + 'x86_64': ['x64', 'x86_64'], + 'arm': ['arm', 'arm'], + 'aarch64': ['aa64', 'aarch64'], + 'riscv64': ['riscv64', 'riscv64'], +}.get(efi_arch, []) + +efi_incdir = get_option('efi-includedir') +found = false +foreach efi_arch_candidate : efi_arch + efi_archdir = efi_incdir / efi_arch_candidate + if cc.has_header(efi_archdir / 'efibind.h', + args: get_option('efi-cflags')) + found = true + break + endif +endforeach + +if not found + if get_option('gnu-efi') == 'true' + error('gnu-efi support requested, but headers not found or efi arch is unknown') + endif + warning('gnu-efi headers not found or efi arch is unknown, disabling gnu-efi support') + subdir_done() +endif + +if not cc.has_header_symbol('efi.h', 'EFI_IMAGE_MACHINE_X64', + args: ['-nostdlib', '-ffreestanding', '-fshort-wchar'] + get_option('efi-cflags'), + include_directories: include_directories(efi_incdir, + efi_archdir)) + + if get_option('gnu-efi') == 'true' + error('gnu-efi support requested, but found headers are too old (3.0.5+ required)') + endif + warning('gnu-efi headers are too old (3.0.5+ required), disabling gnu-efi support') + subdir_done() +endif + +objcopy = run_command(cc.cmd_array(), '-print-prog-name=objcopy', check: true).stdout().strip() +objcopy_2_38 = find_program('objcopy', version: '>=2.38', required: false) + +efi_ld = get_option('efi-ld') +if efi_ld == 'auto' + efi_ld = cc.get_linker_id().split('.')[1] + if efi_ld not in ['bfd', 'gold'] + message('Not using @0@ as efi-ld, falling back to bfd'.format(efi_ld)) + efi_ld = 'bfd' + endif +endif + +efi_multilib = run_command( + cc.cmd_array(), '-print-multi-os-directory', get_option('efi-cflags'), + check: false +).stdout().strip() +efi_multilib = run_command( + 'realpath', '-e', '/usr/lib' / efi_multilib, + check: false +).stdout().strip() + +efi_libdir = '' +foreach dir : [get_option('efi-libdir'), + '/usr/lib/gnuefi' / efi_arch[0], + efi_multilib] + if dir != '' and fs.is_dir(dir) + efi_libdir = dir + break + endif +endforeach +if efi_libdir == '' + if get_option('gnu-efi') == 'true' + error('gnu-efi support requested, but efi-libdir was not found') + endif + warning('efi-libdir was not found, disabling gnu-efi support') + subdir_done() +endif + +efi_lds = '' +foreach location : [ # New locations first introduced with gnu-efi 3.0.11 + [efi_libdir / 'efi.lds', + efi_libdir / 'crt0.o'], + # Older locations... + [efi_libdir / 'gnuefi' / 'elf_@0@_efi.lds'.format(efi_arch[1]), + efi_libdir / 'gnuefi' / 'crt0-efi-@0@.o'.format(efi_arch[1])], + [efi_libdir / 'elf_@0@_efi.lds'.format(efi_arch[1]), + efi_libdir / 'crt0-efi-@0@.o'.format(efi_arch[1])]] + if fs.is_file(location[0]) and fs.is_file(location[1]) + efi_lds = location[0] + efi_crt0 = location[1] + break + endif +endforeach +if efi_lds == '' + if get_option('gnu-efi') == 'true' + error('gnu-efi support requested, but cannot find efi.lds') + endif + warning('efi.lds was not found, disabling gnu-efi support') + subdir_done() +endif + +conf.set10('HAVE_GNU_EFI', true) +conf.set_quoted('EFI_MACHINE_TYPE_NAME', efi_arch[0]) + +efi_conf = configuration_data() +efi_conf.set_quoted('EFI_MACHINE_TYPE_NAME', efi_arch[0]) +efi_conf.set10('ENABLE_TPM', get_option('tpm')) +efi_conf.set10('EFI_TPM_PCR_COMPAT', get_option('efi-tpm-pcr-compat')) + +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 == '' + efi_conf.set('SBAT_DISTRO_VERSION', 'GIT_VERSION') + # This is determined during build, not configuration, so we can't display it yet. + sbat_distro_version_display = '(git version)' + else + efi_conf.set_quoted('SBAT_DISTRO_VERSION', pkgver) + sbat_distro_version_display = pkgver + endif +endif + +efi_config_h = configure_file( + output : 'efi_config.h', + configuration : efi_conf) + +efi_cflags = [ + '-DGNU_EFI_USE_MS_ABI', + '-DSD_BOOT', + '-ffreestanding', + '-fshort-wchar', + '-fvisibility=hidden', + '-I', fundamental_path, + '-I', meson.current_source_dir(), + '-include', efi_config_h, + '-include', version_h, + '-I', efi_archdir, + '-isystem', efi_incdir, + '-std=gnu11', + '-Wall', + '-Wextra', +] + cc.get_supported_arguments( + basic_disabled_warnings + + possible_common_cc_flags + [ + '-fno-stack-protector', + '-fno-strict-aliasing', + '-fpic', + '-fwide-exec-charset=UCS2', + ] +) + +# Our code size has increased enough to possibly create overlapping PE sections +# at sd-stub runtime, which will often enough prevent the image from booting. +# This only happens because the usual instructions for assembling a unified +# kernel image contain hardcoded addresses for section VMAs added in. Until a +# proper solution is in place, we can at least compile with as least -O1 to +# reduce the likelihood of this happening. +# https://github.com/systemd/systemd/issues/24202 +efi_cflags += '-O1' + +efi_cflags += cc.get_supported_arguments({ + 'ia32': ['-mno-sse', '-mno-mmx'], + 'x86_64': ['-mno-red-zone', '-mno-sse', '-mno-mmx'], + 'arm': ['-mgeneral-regs-only', '-mfpu=none'], +}.get(efi_arch[1], [])) + +# We are putting the efi_cc command line together ourselves, so make sure to pull any +# relevant compiler flags from meson/CFLAGS as povided by the user or distro. + +if get_option('werror') + efi_cflags += ['-Werror'] +endif +if get_option('debug') and get_option('mode') == 'developer' + efi_cflags += ['-ggdb', '-DEFI_DEBUG'] +endif +if get_option('optimization') in ['1', '2', '3', 's', 'g'] + efi_cflags += ['-O' + get_option('optimization')] +endif +if get_option('b_ndebug') == 'true' or ( + get_option('b_ndebug') == 'if-release' and get_option('buildtype') in ['plain', 'release']) + efi_cflags += ['-DNDEBUG'] +endif +if get_option('b_lto') + efi_cflags += cc.has_argument('-flto=auto') ? ['-flto=auto'] : ['-flto'] +endif + +foreach arg : get_option('c_args') + if arg in [ + '-DNDEBUG', + '-fno-lto', + '-O1', '-O2', '-O3', '-Og', '-Os', + '-Werror', + ] or arg.split('=')[0] in [ + '-ffile-prefix-map', + '-flto', + ] or (get_option('mode') == 'developer' and arg in [ + '-DEFI_DEBUG', + '-g', '-ggdb', + ]) + + message('Using "@0@" from c_args for EFI compiler'.format(arg)) + efi_cflags += arg + endif +endforeach + +efi_cflags += get_option('efi-cflags') + +efi_ldflags = [ + '-fuse-ld=' + efi_ld, + '-L', efi_libdir, + '-nostdlib', + '-T', efi_lds, + '-Wl,--build-id=sha1', + '-Wl,--fatal-warnings', + '-Wl,--no-undefined', + '-Wl,--warn-common', + '-Wl,-Bsymbolic', + '-z', 'nocombreloc', + '-z', 'noexecstack', + efi_crt0, +] + +foreach arg : ['-Wl,--no-warn-execstack', + '-Wl,--no-warn-rwx-segments'] + # We need to check the correct linker for supported args. This is what + # cc.has_multi_link_arguments() is for, but it helpfully overrides our + # choice of linker by putting its own -fuse-ld= arg after ours. + if run_command('bash', '-c', + 'exec "$@" -x c -o/dev/null <(echo "int main(void){return 0;}")' + + ' -fuse-ld=' + efi_ld + ' -Wl,--fatal-warnings ' + arg, + 'bash', cc.cmd_array(), + check : false).returncode() == 0 + efi_ldflags += arg + endif +endforeach + +# If using objcopy, crt0 must not include the PE/COFF header +if run_command('grep', '-q', 'coff_header', efi_crt0, check: false).returncode() == 0 + coff_header_in_crt0 = true +else + coff_header_in_crt0 = false +endif + +if efi_arch[1] in ['arm', 'riscv64'] or (efi_arch[1] == 'aarch64' and (not objcopy_2_38.found() or coff_header_in_crt0)) + efi_ldflags += ['-shared'] + # ARM32 and 64bit RISC-V don't have an EFI capable objcopy. + # Older objcopy doesn't support Aarch64 either. + # Use 'binary' instead, and add required symbols manually. + efi_ldflags += ['-Wl,--defsym=EFI_SUBSYSTEM=0xa'] + efi_format = ['-O', 'binary'] +else + efi_ldflags += ['-pie'] + if efi_ld == 'bfd' + efi_ldflags += '-Wl,--no-dynamic-linker' + endif + efi_format = ['--target=efi-app-@0@'.format(efi_arch[1])] +endif + +if efi_arch[1] == 'arm' + # On arm, the compiler (correctly) warns about wchar_t size mismatch. This + # is because libgcc is not compiled with -fshort-wchar, but it does not + # have any occurrences of wchar_t in its sources or the documentation, so + # it is safe to assume that we can ignore this warning. + efi_ldflags += ['-Wl,--no-wchar-size-warning'] +endif + +if run_command('grep', '-q', '__CTOR_LIST__', efi_lds, check: false).returncode() == 0 + # fedora has a patched gnu-efi that adds support for ELF constructors. + # If ld is called by gcc something about these symbols breaks, resulting + # in sd-boot freezing when gnu-efi runs the constructors. Force defining + # them seems to work around this. + efi_ldflags += [ + '-Wl,--defsym=_init_array=0', + '-Wl,--defsym=_init_array_end=0', + '-Wl,--defsym=_fini_array=0', + '-Wl,--defsym=_fini_array_end=0', + '-Wl,--defsym=__CTOR_LIST__=0', + '-Wl,--defsym=__CTOR_END__=0', + '-Wl,--defsym=__DTOR_LIST__=0', + '-Wl,--defsym=__DTOR_END__=0', + ] +endif + +if cc.get_id() == 'clang' and cc.version().split('.')[0].to_int() <= 10 + # clang <= 10 doesn't pass -T to the linker and then even complains about it being unused + efi_ldflags += ['-Wl,-T,' + efi_lds, '-Wno-unused-command-line-argument'] +endif + +summary({ + 'EFI machine type' : efi_arch[0], + 'EFI LD' : efi_ld, + 'EFI lds' : efi_lds, + 'EFI crt0' : efi_crt0, + 'EFI include directory' : efi_archdir}, + section : 'Extensible Firmware Interface') + +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': sbat_distro_version_display, + 'SBAT distro summary': efi_conf.get('SBAT_DISTRO_SUMMARY'), + 'SBAT distro URL': efi_conf.get('SBAT_DISTRO_URL')}, + section : 'Extensible Firmware Interface') +endif + +############################################################ + +efi_headers = files( + 'bcd.h', + 'console.h', + 'cpio.h', + 'devicetree.h', + 'disk.h', + 'drivers.h', + 'efi-string.h', + 'graphics.h', + 'initrd.h', + 'linux.h', + 'measure.h', + 'missing_efi.h', + 'pe.h', + 'random-seed.h', + 'secure-boot.h', + 'shim.h', + 'splash.h', + 'ticks.h', + 'util.h', + 'xbootldr.h', +) + +common_sources = files( + 'assert.c', + 'console.c', + 'devicetree.c', + 'disk.c', + 'efi-string.c', + 'graphics.c', + 'initrd.c', + 'measure.c', + 'pe.c', + 'secure-boot.c', + 'ticks.c', + 'util.c', +) + +systemd_boot_sources = files( + 'boot.c', + 'drivers.c', + 'random-seed.c', + 'vmm.c', + 'shim.c', + 'xbootldr.c', +) + +stub_sources = files( + 'cpio.c', + 'linux.c', + 'splash.c', + 'stub.c', +) + +if efi_arch[1] in ['ia32', 'x86_64'] + stub_sources += files('linux_x86.c') +endif + +tests += [ + [files('test-efi-string.c', 'efi-string.c')], +] + +# BCD parser only makes sense on arches that Windows supports. +if efi_arch[1] in ['ia32', 'x86_64', 'arm', 'aarch64'] + systemd_boot_sources += files('bcd.c') + tests += [ + [files('test-bcd.c', 'efi-string.c'), + [], + [libzstd], + [], + 'HAVE_ZSTD'], + ] + fuzzers += [ + [files('fuzz-bcd.c', 'bcd.c', 'efi-string.c')], + [files('fuzz-efi-string.c', 'efi-string.c')], + ] +endif + +systemd_boot_objects = [] +stub_objects = [] +foreach file : fundamental_source_paths + common_sources + systemd_boot_sources + stub_sources + # FIXME: replace ''.format(file) with fs.name(file) when meson_version requirement is >= 0.59.0 + o_file = custom_target('@0@.o'.format(file).split('/')[-1], + input : file, + output : '@0@.o'.format(file).split('/')[-1], + command : [cc.cmd_array(), '-c', '@INPUT@', '-o', '@OUTPUT@', efi_cflags], + depend_files : efi_headers + fundamental_headers) + if (fundamental_source_paths + common_sources + systemd_boot_sources).contains(file) + systemd_boot_objects += o_file + endif + if (fundamental_source_paths + common_sources + stub_sources).contains(file) + stub_objects += o_file + endif +endforeach + +foreach tuple : [['systemd-boot@0@.@1@', systemd_boot_objects, false, 'systemd-boot'], + ['linux@0@.@1@.stub', stub_objects, true, 'systemd-stub']] + elf = custom_target( + tuple[0].format(efi_arch[0], 'elf'), + input : tuple[1], + output : tuple[0].format(efi_arch[0], 'elf'), + command : [cc.cmd_array(), + '-o', '@OUTPUT@', + efi_cflags, + efi_ldflags, + '@INPUT@', + '-lefi', + '-lgnuefi', + '-lgcc'], + install : tuple[2], + install_tag: tuple[3], + install_dir : bootlibdir) + + efi = custom_target( + tuple[0].format(efi_arch[0], 'efi'), + input : elf, + output : tuple[0].format(efi_arch[0], 'efi'), + command : [objcopy, + '-j', '.bss*', + '-j', '.data', + '-j', '.dynamic', + '-j', '.dynsym', + '-j', '.osrel', + '-j', '.rel*', + '-j', '.sbat', + '-j', '.sdata', + '-j', '.sdmagic', + '-j', '.text', + '--section-alignment=512', + efi_format, + '@INPUT@', '@OUTPUT@'], + install : true, + install_tag: tuple[3], + install_dir : bootlibdir) + + alias_target(tuple[3], efi) +endforeach diff --git a/src/boot/efi/missing_efi.h b/src/boot/efi/missing_efi.h new file mode 100644 index 0000000..250c84c --- /dev/null +++ b/src/boot/efi/missing_efi.h @@ -0,0 +1,400 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> + +#include "macro-fundamental.h" + +/* gnu-efi 3.0.13 */ +#ifndef EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID + +#define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \ + { 0xdd9e7534, 0x7762, 0x4698, {0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa} } +#define SimpleTextInputExProtocol ((EFI_GUID)EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID) + +#define EFI_SHIFT_STATE_VALID 0x80000000 +#define EFI_RIGHT_SHIFT_PRESSED 0x00000001 +#define EFI_LEFT_SHIFT_PRESSED 0x00000002 +#define EFI_RIGHT_CONTROL_PRESSED 0x00000004 +#define EFI_LEFT_CONTROL_PRESSED 0x00000008 +#define EFI_RIGHT_ALT_PRESSED 0x00000010 +#define EFI_LEFT_ALT_PRESSED 0x00000020 +#define EFI_RIGHT_LOGO_PRESSED 0x00000040 +#define EFI_LEFT_LOGO_PRESSED 0x00000080 + +struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; + +typedef EFI_STATUS (EFIAPI *EFI_INPUT_RESET_EX)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + BOOLEAN ExtendedVerification +); + +typedef UINT8 EFI_KEY_TOGGLE_STATE; + +typedef struct { + UINT32 KeyShiftState; + EFI_KEY_TOGGLE_STATE KeyToggleState; +} EFI_KEY_STATE; + +typedef struct { + EFI_INPUT_KEY Key; + EFI_KEY_STATE KeyState; +} EFI_KEY_DATA; + +typedef EFI_STATUS (EFIAPI *EFI_INPUT_READ_KEY_EX)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + EFI_KEY_DATA *KeyData +); + +typedef EFI_STATUS (EFIAPI *EFI_SET_STATE)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + EFI_KEY_TOGGLE_STATE *KeyToggleState +); + +typedef EFI_STATUS (EFIAPI *EFI_KEY_NOTIFY_FUNCTION)( + EFI_KEY_DATA *KeyData +); + +typedef EFI_STATUS (EFIAPI *EFI_REGISTER_KEYSTROKE_NOTIFY)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + EFI_KEY_DATA KeyData, + EFI_KEY_NOTIFY_FUNCTION KeyNotificationFunction, + VOID **NotifyHandle +); + +typedef EFI_STATUS (EFIAPI *EFI_UNREGISTER_KEYSTROKE_NOTIFY)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This, + VOID *NotificationHandle +); + +typedef struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL { + EFI_INPUT_RESET_EX Reset; + EFI_INPUT_READ_KEY_EX ReadKeyStrokeEx; + EFI_EVENT WaitForKeyEx; + EFI_SET_STATE SetState; + EFI_REGISTER_KEYSTROKE_NOTIFY RegisterKeyNotify; + EFI_UNREGISTER_KEYSTROKE_NOTIFY UnregisterKeyNotify; +} EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; + +#endif + +/* gnu-efi 3.0.14 */ +#ifndef EFI_IMAGE_MACHINE_RISCV64 + #define EFI_IMAGE_MACHINE_RISCV64 0x5064 +#endif + +/* gnu-efi 3.0.14 */ +#ifndef EFI_DTB_TABLE_GUID +#define EFI_DTB_TABLE_GUID \ + { 0xb1b621d5, 0xf19c, 0x41a5, {0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0} } +#define EfiDtbTableGuid ((EFI_GUID)EFI_DTB_TABLE_GUID) +#endif + +#ifndef EFI_DT_FIXUP_PROTOCOL_GUID +#define EFI_DT_FIXUP_PROTOCOL_GUID \ + { 0xe617d64c, 0xfe08, 0x46da, {0xf4, 0xdc, 0xbb, 0xd5, 0x87, 0x0c, 0x73, 0x00} } +#define EfiDtFixupProtocol ((EFI_GUID)EFI_DT_FIXUP_PROTOCOL_GUID) + +#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; + +typedef EFI_STATUS (EFIAPI *EFI_DT_FIXUP) ( + IN EFI_DT_FIXUP_PROTOCOL *This, + IN VOID *Fdt, + IN OUT UINTN *BufferSize, + IN UINT32 Flags); + +struct _EFI_DT_FIXUP_PROTOCOL { + UINT64 Revision; + EFI_DT_FIXUP Fixup; +}; + +#endif + +/* TCG EFI Protocol Specification */ +#ifndef EFI_TCG_GUID + +#define EFI_TCG_GUID \ + &(const EFI_GUID) { 0xf541796d, 0xa62e, 0x4954, { 0xa7, 0x75, 0x95, 0x84, 0xf6, 0x1b, 0x9c, 0xdd } } + +typedef struct _TCG_VERSION { + UINT8 Major; + UINT8 Minor; + UINT8 RevMajor; + UINT8 RevMinor; +} TCG_VERSION; + +typedef struct tdEFI_TCG2_VERSION { + UINT8 Major; + UINT8 Minor; +} EFI_TCG2_VERSION; + +typedef struct _TCG_BOOT_SERVICE_CAPABILITY { + UINT8 Size; + struct _TCG_VERSION StructureVersion; + struct _TCG_VERSION ProtocolSpecVersion; + UINT8 HashAlgorithmBitmap; + BOOLEAN TPMPresentFlag; + BOOLEAN TPMDeactivatedFlag; +} TCG_BOOT_SERVICE_CAPABILITY; + +typedef struct tdTREE_BOOT_SERVICE_CAPABILITY { + UINT8 Size; + EFI_TCG2_VERSION StructureVersion; + EFI_TCG2_VERSION ProtocolVersion; + UINT32 HashAlgorithmBitmap; + UINT32 SupportedEventLogs; + BOOLEAN TrEEPresentFlag; + UINT16 MaxCommandSize; + UINT16 MaxResponseSize; + UINT32 ManufacturerID; +} TREE_BOOT_SERVICE_CAPABILITY; + +typedef UINT32 TCG_ALGORITHM_ID; +#define TCG_ALG_SHA 0x00000004 // The SHA1 algorithm + +#define SHA1_DIGEST_SIZE 20 + +typedef struct _TCG_DIGEST { + UINT8 Digest[SHA1_DIGEST_SIZE]; +} TCG_DIGEST; + +#define EV_IPL 13 + +typedef struct _TCG_PCR_EVENT { + UINT32 PCRIndex; + UINT32 EventType; + struct _TCG_DIGEST digest; + UINT32 EventSize; + UINT8 Event[1]; +} TCG_PCR_EVENT; + +INTERFACE_DECL(_EFI_TCG); + +typedef EFI_STATUS(EFIAPI * EFI_TCG_STATUS_CHECK) (IN struct _EFI_TCG * This, + OUT struct _TCG_BOOT_SERVICE_CAPABILITY * ProtocolCapability, + OUT UINT32 * TCGFeatureFlags, + OUT EFI_PHYSICAL_ADDRESS * EventLogLocation, + OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry); + +typedef EFI_STATUS(EFIAPI * EFI_TCG_HASH_ALL) (IN struct _EFI_TCG * This, + IN UINT8 * HashData, + IN UINT64 HashDataLen, + IN TCG_ALGORITHM_ID AlgorithmId, + IN OUT UINT64 * HashedDataLen, IN OUT UINT8 ** HashedDataResult); + +typedef EFI_STATUS(EFIAPI * EFI_TCG_LOG_EVENT) (IN struct _EFI_TCG * This, + IN struct _TCG_PCR_EVENT * TCGLogData, + IN OUT UINT32 * EventNumber, IN UINT32 Flags); + +typedef EFI_STATUS(EFIAPI * EFI_TCG_PASS_THROUGH_TO_TPM) (IN struct _EFI_TCG * This, + IN UINT32 TpmInputParameterBlockSize, + IN UINT8 * TpmInputParameterBlock, + IN UINT32 TpmOutputParameterBlockSize, + IN UINT8 * TpmOutputParameterBlock); + +typedef EFI_STATUS(EFIAPI * EFI_TCG_HASH_LOG_EXTEND_EVENT) (IN struct _EFI_TCG * This, + IN EFI_PHYSICAL_ADDRESS HashData, + IN UINT64 HashDataLen, + IN TCG_ALGORITHM_ID AlgorithmId, + IN struct _TCG_PCR_EVENT * TCGLogData, + IN OUT UINT32 * EventNumber, + OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry); + +typedef struct _EFI_TCG { + EFI_TCG_STATUS_CHECK StatusCheck; + EFI_TCG_HASH_ALL HashAll; + EFI_TCG_LOG_EVENT LogEvent; + EFI_TCG_PASS_THROUGH_TO_TPM PassThroughToTPM; + EFI_TCG_HASH_LOG_EXTEND_EVENT HashLogExtendEvent; +} EFI_TCG; + +#endif + +/* TCG EFI Protocol Specification */ +#ifndef EFI_TCG2_GUID + +#define EFI_TCG2_GUID \ + &(const EFI_GUID) { 0x607f766c, 0x7455, 0x42be, { 0x93, 0x0b, 0xe4, 0xd7, 0x6d, 0xb2, 0x72, 0x0f } } + +typedef struct tdEFI_TCG2_PROTOCOL EFI_TCG2_PROTOCOL; + +typedef UINT32 EFI_TCG2_EVENT_LOG_BITMAP; +typedef UINT32 EFI_TCG2_EVENT_LOG_FORMAT; +typedef UINT32 EFI_TCG2_EVENT_ALGORITHM_BITMAP; + +typedef struct tdEFI_TCG2_BOOT_SERVICE_CAPABILITY { + UINT8 Size; + EFI_TCG2_VERSION StructureVersion; + EFI_TCG2_VERSION ProtocolVersion; + EFI_TCG2_EVENT_ALGORITHM_BITMAP HashAlgorithmBitmap; + EFI_TCG2_EVENT_LOG_BITMAP SupportedEventLogs; + BOOLEAN TPMPresentFlag; + UINT16 MaxCommandSize; + UINT16 MaxResponseSize; + UINT32 ManufacturerID; + UINT32 NumberOfPCRBanks; + EFI_TCG2_EVENT_ALGORITHM_BITMAP ActivePcrBanks; +} EFI_TCG2_BOOT_SERVICE_CAPABILITY; + +#define EFI_TCG2_EVENT_HEADER_VERSION 1 + +typedef struct { + UINT32 HeaderSize; + UINT16 HeaderVersion; + UINT32 PCRIndex; + UINT32 EventType; +} _packed_ EFI_TCG2_EVENT_HEADER; + +typedef struct tdEFI_TCG2_EVENT { + UINT32 Size; + EFI_TCG2_EVENT_HEADER Header; + UINT8 Event[1]; +} _packed_ EFI_TCG2_EVENT; + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_CAPABILITY) (IN EFI_TCG2_PROTOCOL * This, + IN OUT EFI_TCG2_BOOT_SERVICE_CAPABILITY * ProtocolCapability); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_EVENT_LOG) (IN EFI_TCG2_PROTOCOL * This, + IN EFI_TCG2_EVENT_LOG_FORMAT EventLogFormat, + OUT EFI_PHYSICAL_ADDRESS * EventLogLocation, + OUT EFI_PHYSICAL_ADDRESS * EventLogLastEntry, + OUT BOOLEAN * EventLogTruncated); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_HASH_LOG_EXTEND_EVENT) (IN EFI_TCG2_PROTOCOL * This, + IN UINT64 Flags, + IN EFI_PHYSICAL_ADDRESS DataToHash, + IN UINT64 DataToHashLen, IN EFI_TCG2_EVENT * EfiTcgEvent); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_SUBMIT_COMMAND) (IN EFI_TCG2_PROTOCOL * This, + IN UINT32 InputParameterBlockSize, + IN UINT8 * InputParameterBlock, + IN UINT32 OutputParameterBlockSize, IN UINT8 * OutputParameterBlock); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This, OUT UINT32 * ActivePcrBanks); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_SET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This, IN UINT32 ActivePcrBanks); + +typedef EFI_STATUS(EFIAPI * EFI_TCG2_GET_RESULT_OF_SET_ACTIVE_PCR_BANKS) (IN EFI_TCG2_PROTOCOL * This, + OUT UINT32 * OperationPresent, OUT UINT32 * Response); + +typedef struct tdEFI_TCG2_PROTOCOL { + EFI_TCG2_GET_CAPABILITY GetCapability; + EFI_TCG2_GET_EVENT_LOG GetEventLog; + EFI_TCG2_HASH_LOG_EXTEND_EVENT HashLogExtendEvent; + EFI_TCG2_SUBMIT_COMMAND SubmitCommand; + EFI_TCG2_GET_ACTIVE_PCR_BANKS GetActivePcrBanks; + EFI_TCG2_SET_ACTIVE_PCR_BANKS SetActivePcrBanks; + EFI_TCG2_GET_RESULT_OF_SET_ACTIVE_PCR_BANKS GetResultOfSetActivePcrBanks; +} EFI_TCG2; + +#endif + +#ifndef EFI_LOAD_FILE2_PROTOCOL_GUID +#define EFI_LOAD_FILE2_PROTOCOL_GUID \ + {0x4006c0c1, 0xfcb3, 0x403e, {0x99, 0x6d, 0x4a, 0x6c, 0x87, 0x24, 0xe0, 0x6d} } +#define EfiLoadFile2Protocol ((EFI_GUID)EFI_LOAD_FILE2_PROTOCOL_GUID) +#endif + +#define LINUX_INITRD_MEDIA_GUID \ + {0x5568e427, 0x68fc, 0x4f3d, {0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68} } + +/* UEFI Platform Initialization (Vol2: DXE) */ +#ifndef EFI_SECURITY_ARCH_PROTOCOL_GUID + +#define EFI_SECURITY_ARCH_PROTOCOL_GUID \ + { 0xa46423e3, 0x4617, 0x49f1, { 0xb9, 0xff, 0xd1, 0xbf, 0xa9, 0x11, 0x58, 0x39 } } +#define EFI_SECURITY2_ARCH_PROTOCOL_GUID \ + { 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); + +struct EFI_SECURITY_ARCH_PROTOCOL { + EFI_SECURITY_FILE_AUTHENTICATION_STATE FileAuthenticationState; +}; + +typedef EFI_STATUS (EFIAPI *EFI_SECURITY2_FILE_AUTHENTICATION)( + const EFI_SECURITY2_ARCH_PROTOCOL *This, + const EFI_DEVICE_PATH *DevicePath, + void *FileBuffer, + UINTN FileSize, + BOOLEAN BootPolicy); + +struct EFI_SECURITY2_ARCH_PROTOCOL { + EFI_SECURITY2_FILE_AUTHENTICATION FileAuthentication; +}; + +#endif + +#ifndef EFI_CONSOLE_CONTROL_GUID + +#define EFI_CONSOLE_CONTROL_GUID \ + &(const EFI_GUID) { 0xf42f7782, 0x12e, 0x4c12, { 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21 } } + +struct _EFI_CONSOLE_CONTROL_PROTOCOL; + +typedef enum { + EfiConsoleControlScreenText, + EfiConsoleControlScreenGraphics, + EfiConsoleControlScreenMaxValue, +} EFI_CONSOLE_CONTROL_SCREEN_MODE; + +typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE *Mode, + BOOLEAN *UgaExists, + BOOLEAN *StdInLocked +); + +typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE Mode +); + +typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + CHAR16 *Password +); + +typedef struct _EFI_CONSOLE_CONTROL_PROTOCOL { + EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE GetMode; + EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE SetMode; + EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN LockStdIn; +} EFI_CONSOLE_CONTROL_PROTOCOL; + +#endif + +#ifndef EFI_IMAGE_SECURITY_DATABASE_VARIABLE + +#define EFI_IMAGE_SECURITY_DATABASE_VARIABLE \ + { 0xd719b2cb, 0x3d3a, 0x4596, {0xa3, 0xbc, 0xda, 0xd0, 0xe, 0x67, 0x65, 0x6f }} + +#endif + +#ifndef EFI_SHELL_PARAMETERS_PROTOCOL_GUID +# define EFI_SHELL_PARAMETERS_PROTOCOL_GUID \ + { 0x752f3136, 0x4e16, 0x4fdc, { 0xa2, 0x2a, 0xe5, 0xf4, 0x68, 0x12, 0xf4, 0xca } } + +typedef struct { + CHAR16 **Argv; + UINTN Argc; + void *StdIn; + void *StdOut; + void *StdErr; +} EFI_SHELL_PARAMETERS_PROTOCOL; +#endif diff --git a/src/boot/efi/pe.c b/src/boot/efi/pe.c new file mode 100644 index 0000000..3d5da14 --- /dev/null +++ b/src/boot/efi/pe.c @@ -0,0 +1,338 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "missing_efi.h" +#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 EFI_IMAGE_MACHINE_IA32 +# define TARGET_MACHINE_TYPE_COMPATIBILITY EFI_IMAGE_MACHINE_X64 +#elif defined(__x86_64__) +# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_X64 +#elif defined(__aarch64__) +# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_AARCH64 +#elif defined(__arm__) +# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_ARMTHUMB_MIXED +#elif defined(__riscv) && __riscv_xlen == 64 +# define TARGET_MACHINE_TYPE EFI_IMAGE_MACHINE_RISCV64 +#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 inline bool verify_dos(const DosFileHeader *dos) { + assert(dos); + return memcmp(dos->Magic, DOS_FILE_MAGIC, STRLEN(DOS_FILE_MAGIC)) == 0; +} + +static inline 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 inline UINTN 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[], + UINTN n_table, + const char * const sections[], + UINTN *offsets, + UINTN *sizes, + bool in_memory) { + + assert(section_table); + assert(sections); + assert(offsets); + assert(sizes); + + size_t prev_section_addr = 0; + + for (UINTN i = 0; i < n_table; i++) { + const PeSectionHeader *sect = section_table + i; + + if (in_memory) { + if (prev_section_addr > sect->VirtualAddress) + log_error_stall(u"Overlapping PE sections detected. Boot may fail due to image memory corruption!"); + prev_section_addr = sect->VirtualAddress + sect->VirtualSize; + } + + for (UINTN 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) { + UINTN 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 64bit kernel on 32bit EFI that is otherwise running on a 64bit 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[], UINTN *addrs, UINTN *sizes) { + const DosFileHeader *dos; + const PeFileHeader *pe; + UINTN 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[], + UINTN *offsets, + UINTN *sizes) { + _cleanup_free_ PeSectionHeader *section_table = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + DosFileHeader dos; + PeFileHeader pe; + UINTN 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..ff7ff47 --- /dev/null +++ b/src/boot/efi/pe.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efidef.h> +#include <uchar.h> + +EFI_STATUS pe_memory_locate_sections( + const void *base, + const char * const sections[], + UINTN *addrs, + UINTN *sizes); + +EFI_STATUS pe_file_locate_sections( + EFI_FILE *dir, + const char16_t *path, + const char * const sections[], + UINTN *offsets, + UINTN *sizes); + +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address); diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c new file mode 100644 index 0000000..aea4f7e --- /dev/null +++ b/src/boot/efi/random-seed.c @@ -0,0 +1,321 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "missing_efi.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) + +#define EFI_RNG_GUID &(const EFI_GUID) EFI_RNG_PROTOCOL_GUID + +/* SHA256 gives us 256/8=32 bytes */ +#define HASH_VALUE_SIZE 32 + +static EFI_STATUS acquire_rng(UINTN size, void **ret) { + _cleanup_free_ void *data = NULL; + EFI_RNG_PROTOCOL *rng; + EFI_STATUS err; + + assert(ret); + + /* Try to acquire the specified number of bytes from the UEFI RNG */ + + err = BS->LocateProtocol((EFI_GUID *) EFI_RNG_GUID, NULL, (void **) &rng); + if (err != EFI_SUCCESS) + return err; + if (!rng) + return EFI_UNSUPPORTED; + + data = xmalloc(size); + + err = rng->GetRNG(rng, NULL, size, data); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to acquire RNG data: %r", err); + + *ret = TAKE_PTR(data); + return EFI_SUCCESS; +} + +static void hash_once( + const void *old_seed, + const void *rng, + UINTN size, + const void *system_token, + UINTN system_token_size, + uint64_t uefi_monotonic_counter, + UINTN counter, + uint8_t ret[static HASH_VALUE_SIZE]) { + + /* This hashes together: + * + * 1. The contents of the old seed file + * 2. Some random data acquired from the UEFI RNG (optional) + * 3. Some 'system token' the installer installed as EFI variable (optional) + * 4. The UEFI "monotonic counter" that increases with each boot + * 5. A supplied counter value + * + * And writes the result to the specified buffer. + */ + + struct sha256_ctx hash; + + assert(old_seed); + assert(system_token_size == 0 || system_token); + + sha256_init_ctx(&hash); + sha256_process_bytes(old_seed, size, &hash); + if (rng) + sha256_process_bytes(rng, size, &hash); + if (system_token_size > 0) + sha256_process_bytes(system_token, system_token_size, &hash); + sha256_process_bytes(&uefi_monotonic_counter, sizeof(uefi_monotonic_counter), &hash); + sha256_process_bytes(&counter, sizeof(counter), &hash); + sha256_finish_ctx(&hash, ret); +} + +static EFI_STATUS hash_many( + const void *old_seed, + const void *rng, + UINTN size, + const void *system_token, + UINTN system_token_size, + uint64_t uefi_monotonic_counter, + UINTN counter_start, + UINTN n, + void **ret) { + + _cleanup_free_ void *output = NULL; + + assert(old_seed); + assert(system_token_size == 0 || system_token); + assert(ret); + + /* Hashes the specified parameters in counter mode, generating n hash values, with the counter in the + * range counter_start…counter_start+n-1. */ + + output = xmalloc_multiply(HASH_VALUE_SIZE, n); + + for (UINTN i = 0; i < n; i++) + hash_once(old_seed, rng, size, + system_token, system_token_size, + uefi_monotonic_counter, + counter_start + i, + (uint8_t*) output + (i * HASH_VALUE_SIZE)); + + *ret = TAKE_PTR(output); + return EFI_SUCCESS; +} + +static EFI_STATUS mangle_random_seed( + const void *old_seed, + const void *rng, + UINTN size, + const void *system_token, + UINTN system_token_size, + uint64_t uefi_monotonic_counter, + void **ret_new_seed, + void **ret_for_kernel) { + + _cleanup_free_ void *new_seed = NULL, *for_kernel = NULL; + EFI_STATUS err; + UINTN n; + + assert(old_seed); + assert(system_token_size == 0 || system_token); + assert(ret_new_seed); + assert(ret_for_kernel); + + /* This takes the old seed file contents, an (optional) random number acquired from the UEFI RNG, an + * (optional) system 'token' installed once by the OS installer in an EFI variable, and hashes them + * together in counter mode, generating a new seed (to replace the file on disk) and the seed for the + * kernel. To keep things simple, the new seed and kernel data have the same size as the old seed and + * RNG data. */ + + n = (size + HASH_VALUE_SIZE - 1) / HASH_VALUE_SIZE; + + /* Begin hashing in counter mode at counter 0 for the new seed for the disk */ + err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, 0, n, &new_seed); + if (err != EFI_SUCCESS) + return err; + + /* Continue counting at 'n' for the seed for the kernel */ + err = hash_many(old_seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, n, n, &for_kernel); + if (err != EFI_SUCCESS) + return err; + + *ret_new_seed = TAKE_PTR(new_seed); + *ret_for_kernel = TAKE_PTR(for_kernel); + + return EFI_SUCCESS; +} + +static EFI_STATUS acquire_system_token(void **ret, UINTN *ret_size) { + _cleanup_free_ char *data = NULL; + EFI_STATUS err; + UINTN size; + + assert(ret); + assert(ret_size); + + err = efivar_get_raw(LOADER_GUID, L"LoaderSystemToken", &data, &size); + if (err != EFI_SUCCESS) { + if (err != EFI_NOT_FOUND) + log_error_stall(L"Failed to read LoaderSystemToken EFI variable: %r", err); + return err; + } + + if (size <= 0) + return log_error_status_stall(EFI_NOT_FOUND, L"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 (UINTN 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, RandomSeedMode mode) { + _cleanup_free_ void *seed = NULL, *new_seed = NULL, *rng = NULL, *for_kernel = NULL, *system_token = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + UINTN size, rsize, wsize, system_token_size = 0; + _cleanup_free_ EFI_FILE_INFO *info = NULL; + uint64_t uefi_monotonic_counter = 0; + EFI_STATUS err; + + assert(root_dir); + + validate_sha256(); + + if (mode == RANDOM_SEED_OFF) + return EFI_NOT_FOUND; + + /* Let's better be safe than sorry, and for now disable this logic in SecureBoot mode, so that we + * don't credit a random seed that is not authenticated. */ + if (secure_boot_enabled()) + return EFI_NOT_FOUND; + + /* 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. */ + err = acquire_system_token(&system_token, &system_token_size); + if (mode != RANDOM_SEED_ALWAYS && err != EFI_SUCCESS) + return err; + + err = root_dir->Open( + root_dir, + &handle, + (char16_t *) L"\\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_stall(L"Failed to open random seed file: %r", err); + return err; + } + + err = get_file_info_harder(handle, &info, NULL); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to get file info for random seed: %r"); + + size = info->FileSize; + if (size < RANDOM_MAX_SIZE_MIN) + return log_error_status_stall(EFI_INVALID_PARAMETER, L"Random seed file is too short."); + + if (size > RANDOM_MAX_SIZE_MAX) + return log_error_status_stall(EFI_INVALID_PARAMETER, L"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_stall(err, L"Failed to read random seed file: %r", err); + if (rsize != size) + return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short read on random seed file."); + + err = handle->SetPosition(handle, 0); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to seek to beginning of random seed file: %r", err); + + /* 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. */ + (void) acquire_rng(size, &rng); /* It's fine if this fails */ + + /* 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) + return log_error_status_stall(err, L"Failed to acquire UEFI monotonic counter: %r", err); + + /* Calculate new random seed for the disk and what to pass to the kernel */ + err = mangle_random_seed(seed, rng, size, system_token, system_token_size, uefi_monotonic_counter, &new_seed, &for_kernel); + if (err != EFI_SUCCESS) + return err; + + /* Update the random seed on disk before we use it */ + wsize = size; + err = handle->Write(handle, &wsize, new_seed); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to write random seed file: %r", err); + if (wsize != size) + return log_error_status_stall(EFI_PROTOCOL_ERROR, L"Short write on random seed file."); + + err = handle->Flush(handle); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to flush random seed file: %r", err); + + /* We are good to go */ + err = efivar_set_raw(LOADER_GUID, L"LoaderRandomSeed", for_kernel, size, 0); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to write random seed to EFI variable: %r", err); + + 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..6aa1cc5 --- /dev/null +++ b/src/boot/efi/random-seed.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <errno.h> +#include <uchar.h> + +typedef enum RandomSeedMode { + RANDOM_SEED_OFF, + RANDOM_SEED_WITH_SYSTEM_TOKEN, + RANDOM_SEED_ALWAYS, + _RANDOM_SEED_MODE_MAX, + _RANDOM_SEED_MODE_INVALID = -EINVAL, +} RandomSeedMode; + +static const char16_t * const random_seed_modes_table[_RANDOM_SEED_MODE_MAX] = { + [RANDOM_SEED_OFF] = L"off", + [RANDOM_SEED_WITH_SYSTEM_TOKEN] = L"with-system-token", + [RANDOM_SEED_ALWAYS] = L"always", +}; + +EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode); diff --git a/src/boot/efi/secure-boot.c b/src/boot/efi/secure-boot.c new file mode 100644 index 0000000..6212868 --- /dev/null +++ b/src/boot/efi/secure-boot.c @@ -0,0 +1,217 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sbat.h" +#include "secure-boot.h" +#include "console.h" +#include "util.h" + +bool secure_boot_enabled(void) { + bool secure = false; /* avoid false maybe-uninitialized warning */ + EFI_STATUS err; + + err = efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"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(EFI_GLOBAL_GUID, L"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(EFI_GLOBAL_GUID, L"AuditMode", &audit); + (void) efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"DeployedMode", &deployed); + (void) efivar_get_boolean_u8(EFI_GLOBAL_GUID, L"SetupMode", &setup); + + return decode_secure_boot_mode(secure, audit, deployed, setup); +} + +#ifdef SBAT_DISTRO +static const char sbat[] _used_ _section_(".sbat") = SBAT_SECTION_TEXT; +#endif + +EFI_STATUS secure_boot_enroll_at(EFI_FILE *root_dir, const char16_t *path) { + assert(root_dir); + assert(path); + + EFI_STATUS err; + + clear_screen(COLOR_NORMAL); + + Print(L"Enrolling secure boot keys from directory: %s\n" + L"Warning: Enrolling custom Secure Boot keys might soft-brick your machine!\n", + path); + + unsigned timeout_sec = 15; + for(;;) { + /* Enrolling secure boot keys is safe to do in virtualized environments as there is nothing + * we can brick there. */ + if (in_hypervisor()) + break; + + PrintAt(0, ST->ConOut->Mode->CursorRow, L"Enrolling 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_stall(err, L"Error waiting for user input to enroll Secure Boot keys: %r", err); + + /* user aborted, returning EFI_SUCCESS here allows the user to go back to the menu */ + return EFI_SUCCESS; + } + + _cleanup_(file_closep) EFI_FILE *dir = NULL; + + err = open_directory(root_dir, path, &dir); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed opening keys directory %s: %r", path, err); + + 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_VARIABLE, 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_stall(L"Failed reading file %s\\%s: %r", path, sb_vars[i].filename, err); + 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_stall(L"Failed to write %s secure boot variable: %r", sb_vars[i].name, err); + goto out_deallocate; + } + } + + /* 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++) + FreePool(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, + BOOLEAN 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(&(EFI_GUID) EFI_SECURITY_ARCH_PROTOCOL_GUID, 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(&(EFI_GUID) EFI_SECURITY2_ARCH_PROTOCOL_GUID, 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..e98de81 --- /dev/null +++ b/src/boot/efi/secure-boot.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> + +#include "efivars-fundamental.h" +#include "missing_efi.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_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); + +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..ac22433 --- /dev/null +++ b/src/boot/efi/shim.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Port to systemd-boot + * Copyright © 2017 Max Resch <resch.max@gmail.com> + * + * Security Policy Handling + * Copyright © 2012 <James.Bottomley@HansenPartnership.com> + * https://github.com/mjg59/efitools + */ + +#include <efi.h> +#include <efilib.h> + +#include "missing_efi.h" +#include "util.h" +#include "secure-boot.h" +#include "shim.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 \ + &(const EFI_GUID) { 0x605dab50, 0xe046, 0x4300, { 0xab, 0xb6, 0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23 } } + +bool shim_loaded(void) { + struct ShimLock *shim_lock; + + return BS->LocateProtocol((EFI_GUID*) SHIM_LOCK_GUID, 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(&FileSystemProtocol, &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((EFI_GUID *) SHIM_LOCK_GUID, 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; +} diff --git a/src/boot/efi/shim.h b/src/boot/efi/shim.h new file mode 100644 index 0000000..6d213f5 --- /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 <resch.max@gmail.com> + * + * Security Policy Handling + * Copyright © 2012 <James.Bottomley@HansenPartnership.com> + * https://github.com/mjg59/efitools + */ +#pragma once + +#include <efi.h> +#include <stdbool.h> + +bool shim_loaded(void); +EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image); diff --git a/src/boot/efi/splash.c b/src/boot/efi/splash.c new file mode 100644 index 0000000..5bc1084 --- /dev/null +++ b/src/boot/efi/splash.c @@ -0,0 +1,322 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "graphics.h" +#include "splash.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; +} _packed_; + +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, + UINTN size, + struct bmp_dib **ret_dib, + struct bmp_map **ret_map, + const uint8_t **pixmap) { + + struct bmp_file *file; + struct bmp_dib *dib; + struct bmp_map *map; + UINTN row_size; + + assert(bmp); + assert(ret_dib); + assert(ret_map); + assert(pixmap); + + if (size < sizeof(struct bmp_file) + sizeof(struct bmp_dib)) + return EFI_INVALID_PARAMETER; + + /* check file header */ + 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 */ + dib = (struct bmp_dib *)(bmp + sizeof(struct bmp_file)); + if (dib->size < sizeof(struct 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; + } + + row_size = ((UINTN) 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 */ + 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; + UINTN map_size; + + if (dib->colors_used) + map_count = dib->colors_used; + else { + switch (dib->depth) { + case 1: + case 4: + case 8: + map_count = 1 << dib->depth; + break; + + default: + map_count = 0; + break; + } + } + + 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; +} + +static void pixel_blend(uint32_t *dst, const uint32_t source) { + uint32_t alpha, src, src_rb, src_g, dst_rb, dst_g, rb, g; + + assert(dst); + + alpha = (source & 0xff); + + /* convert src from RGBA to XRGB */ + src = source >> 8; + + /* decompose into RB and G components */ + src_rb = (src & 0xff00ff); + src_g = (src & 0x00ff00); + + dst_rb = (*dst & 0xff00ff); + dst_g = (*dst & 0x00ff00); + + /* blend */ + rb = ((((src_rb - dst_rb) * alpha + 0x800080) >> 8) + dst_rb) & 0xff00ff; + g = ((((src_g - dst_g) * alpha + 0x008000) >> 8) + dst_g) & 0x00ff00; + + *dst = (rb | g); +} + +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); + + /* transform and copy pixels */ + in = pixmap; + for (UINTN y = 0; y < dib->y; y++) { + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *out; + UINTN row_size; + + out = &buf[(dib->y - y - 1) * dib->x]; + for (UINTN x = 0; x < dib->x; x++, in++, out++) { + switch (dib->depth) { + case 1: { + for (UINTN 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: { + UINTN i; + + 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 16: { + uint16_t i = *(uint16_t *) in; + + out->Red = (i & 0x7c00) >> 7; + out->Green = (i & 0x3e0) >> 2; + out->Blue = (i & 0x1f) << 3; + in += 1; + break; + } + + case 24: + out->Red = in[2]; + out->Green = in[1]; + out->Blue = in[0]; + in += 2; + break; + + case 32: { + uint32_t i = *(uint32_t *) in; + + pixel_blend((uint32_t *)out, i); + + in += 3; + break; + } + } + } + + /* add row padding; new lines always start at 32 bit boundary */ + row_size = in - pixmap; + in += ((row_size + 3) & ~3) - row_size; + } + + return EFI_SUCCESS; +} + +EFI_STATUS graphics_splash(const uint8_t *content, UINTN 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; + _cleanup_free_ void *blt = NULL; + UINTN x_pos = 0; + UINTN 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(&GraphicsOutputProtocol, 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; + + /* EFI buffer */ + 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..2e502e5 --- /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, UINTN len); diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c new file mode 100644 index 0000000..7c42a16 --- /dev/null +++ b/src/boot/efi/stub.c @@ -0,0 +1,415 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "cpio.h" +#include "devicetree.h" +#include "disk.h" +#include "graphics.h" +#include "linux.h" +#include "measure.h" +#include "pe.h" +#include "secure-boot.h" +#include "splash.h" +#include "tpm-pcr.h" +#include "util.h" + +/* magic string to find in the binary image */ +_used_ _section_(".sdmagic") static const char magic[] = "#### LoaderInfo: systemd-stub " GIT_VERSION " ####"; + +static EFI_STATUS combine_initrd( + EFI_PHYSICAL_ADDRESS initrd_base, UINTN initrd_size, + const void * const extra_initrds[], const size_t extra_initrd_sizes[], size_t n_extra_initrds, + Pages *ret_initr_pages, UINTN *ret_initrd_size) { + + UINTN 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 > UINTN_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) { + UINTN 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) { + memset(p, 0, 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 */ + 0; + + char16_t uuid[37]; + + assert(loaded_image); + + /* Export the device path this image is started from, if it's not set yet */ + if (efivar_get_raw(LOADER_GUID, L"LoaderDevicePartUUID", NULL, NULL) != EFI_SUCCESS) + if (disk_get_part_uuid(loaded_image->DeviceHandle, uuid) == EFI_SUCCESS) + efivar_set(LOADER_GUID, L"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(LOADER_GUID, L"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(LOADER_GUID, L"LoaderImageIdentifier", s, 0); + } + + /* if LoaderFirmwareInfo is not set, let's set it */ + if (efivar_get_raw(LOADER_GUID, L"LoaderFirmwareInfo", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xpool_print(L"%s %u.%02u", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(LOADER_GUID, L"LoaderFirmwareInfo", s, 0); + } + + /* ditto for LoaderFirmwareType */ + if (efivar_get_raw(LOADER_GUID, L"LoaderFirmwareType", NULL, NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *s = NULL; + s = xpool_print(L"UEFI %u.%02u", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(LOADER_GUID, L"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(LOADER_GUID, L"StubInfo", L"systemd-stub " GIT_VERSION, 0); + + (void) efivar_set_uint64_le(LOADER_GUID, L"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. */ + if (secure_boot_enabled() && have_cmdline) + 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, &(EFI_GUID) EFI_SHELL_PARAMETERS_PROTOCOL_GUID, (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 = xpool_print(u"%s %s", old, shell->Argv[i]); + } + + mangle_stub_cmdline(*ret); + return true; +} + +EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { + _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; + size_t linux_size, initrd_size, dt_size; + 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; + int sections_measured = -1, parameters_measured = -1; + bool sysext_measured = false, m; + EFI_STATUS err; + + InitializeLib(image, sys_table); + debug_hook(L"systemd-stub"); + /* Uncomment the next line if you need to wait for debugger. */ + // debug_break(); + + err = BS->OpenProtocol( + image, + &LoadedImageProtocol, + (void **)&loaded_image, + image, + NULL, + EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Error getting a LoadedImageProtocol handle: %r", err); + + 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_stall(err, L"Unable to locate embedded .linux section: %r", err); + } + + /* 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( + TPM_PCR_INDEX_KERNEL_IMAGE, + 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( + TPM_PCR_INDEX_KERNEL_IMAGE, + 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(LOADER_GUID, L"StubPcrKernelImage", TPM_PCR_INDEX_KERNEL_IMAGE, 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 (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); + } + + export_variables(loaded_image); + + if (pack_cpio(loaded_image, + NULL, + L".cred", + ".extra/credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_KERNEL_PARAMETERS, TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT }, + /* n_tpm_pcr= */ 2, + L"Credentials initrd", + &credential_initrd, + &credential_initrd_size, + &m) == EFI_SUCCESS) + parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m); + + if (pack_cpio(loaded_image, + L"\\loader\\credentials", + L".cred", + ".extra/global_credentials", + /* dir_mode= */ 0500, + /* access_mode= */ 0400, + /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_KERNEL_PARAMETERS, TPM_PCR_INDEX_KERNEL_PARAMETERS_COMPAT }, + /* n_tpm_pcr= */ 2, + L"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, + L".raw", + ".extra/sysext", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ (uint32_t[]) { TPM_PCR_INDEX_INITRD_SYSEXTS }, + /* n_tpm_pcr= */ 1, + L"System extension initrd", + &sysext_initrd, + &sysext_initrd_size, + &m) == EFI_SUCCESS) + sysext_measured = m; + + if (parameters_measured > 0) + (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrKernelParameters", TPM_PCR_INDEX_KERNEL_PARAMETERS, 0); + if (sysext_measured) + (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrInitRDSysExts", TPM_PCR_INDEX_INITRD_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", + L"tpm2-pcr-signature.json", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ NULL, + /* n_tpm_pcr= */ 0, + /* 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", + L"tpm2-pcr-public-key.pem", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ NULL, + /* n_tpm_pcr= */ 0, + /* 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; + + dt_size = szs[UNIFIED_SECTION_DTB]; + dt_base = dt_size != 0 ? POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_DTB] : 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); + } + + 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_stall(L"Error loading embedded devicetree: %r", err); + } + + 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; +} diff --git a/src/boot/efi/test-bcd.c b/src/boot/efi/test-bcd.c new file mode 100644 index 0000000..0ee2947 --- /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, char16_strlen(title) * 2); + 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..7b43e1d --- /dev/null +++ b/src/boot/efi/test-efi-string.c @@ -0,0 +1,523 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fnmatch.h> + +#include "efi-string.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]); +} + +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]); +} + +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); +} + +#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(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..889980a --- /dev/null +++ b/src/boot/efi/ticks.c @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> + +#include "ticks.h" +#include "util.h" + +#ifdef __x86_64__ +static uint64_t ticks_read(void) { + uint64_t a, d; + + /* 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; + + __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d)); + return (d << 32) | a; +} +#elif defined(__i386__) +static uint64_t ticks_read(void) { + uint64_t val; + + if (in_hypervisor()) + return 0; + + __asm__ volatile ("rdtsc" : "=A" (val)); + return val; +} +#elif defined(__aarch64__) +static uint64_t ticks_read(void) { + uint64_t val; + asm volatile("mrs %0, cntvct_el0" : "=r"(val)); + return val; +} +#else +static uint64_t ticks_read(void) { + return 0; +} +#endif + +#if defined(__aarch64__) +static uint64_t ticks_freq(void) { + uint64_t freq; + asm volatile("mrs %0, cntfrq_el0" : "=r"(freq)); + return freq; +} +#else +/* count TSC ticks during a millisecond delay */ +static uint64_t ticks_freq(void) { + uint64_t ticks_start, ticks_end; + static uint64_t cache = 0; + + if (cache != 0) + return cache; + + ticks_start = ticks_read(); + BS->Stall(1000); + ticks_end = ticks_read(); + + if (ticks_end < ticks_start) /* Check for an overflow (which is not that unlikely, given on some + * archs the value is 32bit) */ + return 0; + + cache = (ticks_end - ticks_start) * 1000UL; + return cache; +} +#endif + +uint64_t time_usec(void) { + uint64_t ticks, freq; + + ticks = ticks_read(); + if (ticks == 0) + return 0; + + 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 <stdint.h> + +uint64_t time_usec(void); diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c new file mode 100644 index 0000000..66056f0 --- /dev/null +++ b/src/boot/efi/util.c @@ -0,0 +1,794 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> +#if defined(__i386__) || defined(__x86_64__) +# include <cpuid.h> +#endif + +#include "ticks.h" +#include "util.h" + +EFI_STATUS parse_boolean(const char *v, bool *b) { + assert(b); + + if (!v) + return EFI_INVALID_PARAMETER; + + if (streq8(v, "1") || streq8(v, "yes") || streq8(v, "y") || streq8(v, "true") || streq8(v, "t") || + streq8(v, "on")) { + *b = true; + return EFI_SUCCESS; + } + + if (streq8(v, "0") || streq8(v, "no") || streq8(v, "n") || streq8(v, "false") || streq8(v, "f") || + streq8(v, "off")) { + *b = false; + return EFI_SUCCESS; + } + + return EFI_INVALID_PARAMETER; +} + +EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, UINTN 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, UINTN i, uint32_t flags) { + char16_t str[32]; + + assert(vendor); + assert(name); + + /* Note that SPrint has no native sized length specifier and will always use ValueToString() + * regardless of what sign we tell it to use. Therefore, UINTN_MAX will come out as -1 on + * 64bit machines. */ + ValueToString(str, false, 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_get(const EFI_GUID *vendor, const char16_t *name, char16_t **value) { + _cleanup_free_ char16_t *buf = NULL; + EFI_STATUS err; + char16_t *val; + UINTN 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 (!value) + 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) { + *value = 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 */ + + *value = val; + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN *i) { + _cleanup_free_ char16_t *val = NULL; + EFI_STATUS err; + uint64_t u; + + assert(vendor); + assert(name); + assert(i); + + err = efivar_get(vendor, name, &val); + if (err != EFI_SUCCESS) + return err; + + if (!parse_number16(val, &u, NULL) || u > UINTN_MAX) + return EFI_INVALID_PARAMETER; + + *i = 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; + UINTN size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err == EFI_SUCCESS && ret) { + if (size != sizeof(uint32_t)) + return EFI_BUFFER_TOO_SMALL; + + *ret = (uint32_t) buf[0] << 0U | (uint32_t) buf[1] << 8U | (uint32_t) buf[2] << 16U | + (uint32_t) buf[3] << 24U; + } + + return err; +} + +EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) { + _cleanup_free_ char *buf = NULL; + UINTN size; + EFI_STATUS err; + + assert(vendor); + assert(name); + + err = efivar_get_raw(vendor, name, &buf, &size); + if (err == EFI_SUCCESS && ret) { + if (size != sizeof(uint64_t)) + return EFI_BUFFER_TOO_SMALL; + + *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 err; +} + +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **buffer, UINTN *size) { + _cleanup_free_ char *buf = NULL; + UINTN l; + EFI_STATUS err; + + assert(vendor); + assert(name); + + l = sizeof(char16_t *) * EFI_MAXIMUM_VARIABLE_SIZE; + buf = xmalloc(l); + + err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &l, buf); + if (err == EFI_SUCCESS) { + + if (buffer) + *buffer = TAKE_PTR(buf); + + if (size) + *size = l; + } + + return err; +} + +EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret) { + _cleanup_free_ char *b = NULL; + UINTN size; + EFI_STATUS err; + + assert(vendor); + assert(name); + assert(ret); + + err = efivar_get_raw(vendor, name, &b, &size); + if (err == EFI_SUCCESS) + *ret = *b > 0; + + return err; +} + +void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec) { + char16_t str[32]; + + assert(vendor); + assert(name); + + if (usec == 0) + usec = time_usec(); + if (usec == 0) + return; + + /* See comment on ValueToString in efivar_set_uint_string(). */ + ValueToString(str, false, 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; +} + +void mangle_stub_cmdline(char16_t *cmdline) { + char16_t *p = cmdline; + + for (; *cmdline != '\0'; cmdline++) + /* Convert ASCII control characters to spaces. */ + if (*cmdline <= 0x1F) + *cmdline = ' '; + + /* chomp the trailing whitespaces */ + while (cmdline != p) { + --cmdline; + + if (*cmdline != ' ') + break; + + *cmdline = '\0'; + } +} + +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, UINTN off, UINTN size, char **ret, UINTN *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_harder(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. */ + UINTN 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. */ + memset(buf + size, 0, extra); + + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; + + return err; +} + +void log_error_stall(const char16_t *fmt, ...) { + va_list args; + + assert(fmt); + + int32_t attr = ST->ConOut->Mode->Attribute; + ST->ConOut->SetAttribute(ST->ConOut, EFI_LIGHTRED|EFI_BACKGROUND_BLACK); + + if (ST->ConOut->Mode->CursorColumn > 0) + Print(L"\n"); + + va_start(args, fmt); + VPrint(fmt, args); + va_end(args); + + Print(L"\n"); + + ST->ConOut->SetAttribute(ST->ConOut, attr); + + /* Give the user a chance to see the message. */ + BS->Stall(3 * 1000 * 1000); +} + +EFI_STATUS log_oom(void) { + log_error_stall(L"Out of memory."); + return EFI_OUT_OF_RESOURCES; +} + +void print_at(UINTN x, UINTN y, UINTN 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(UINTN attr) { + ST->ConOut->SetAttribute(ST->ConOut, attr); + ST->ConOut->ClearScreen(ST->ConOut); +} + +void sort_pointer_array( + void **array, + UINTN n_members, + compare_pointer_func_t compare) { + + assert(array || n_members == 0); + assert(compare); + + if (n_members <= 1) + return; + + for (UINTN i = 1; i < n_members; i++) { + UINTN 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_harder( + EFI_FILE *handle, + EFI_FILE_INFO **ret, + UINTN *ret_size) { + + UINTN size = offsetof(EFI_FILE_INFO, FileName) + 256; + _cleanup_free_ EFI_FILE_INFO *fi = NULL; + EFI_STATUS err; + + assert(handle); + assert(ret); + + /* A lot like LibFileInfo() but with useful error propagation */ + + fi = xmalloc(size); + err = handle->GetInfo(handle, &GenericFileInfo, &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, &GenericFileInfo, &size, fi); + } + + if (err != EFI_SUCCESS) + return err; + + *ret = TAKE_PTR(fi); + + if (ret_size) + *ret_size = size; + + return EFI_SUCCESS; +} + +EFI_STATUS readdir_harder( + EFI_FILE *handle, + EFI_FILE_INFO **buffer, + UINTN *buffer_size) { + + EFI_STATUS err; + UINTN 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) { + /* 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, start with a buffer that should handle FAT32 max + * file name length. + * As a side effect, most readdir_harder() calls will now be slightly faster. */ + sz = sizeof(EFI_FILE_INFO) + 256 * sizeof(char16_t); + *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_harder(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(EFI_GLOBAL_GUID, L"OsIndicationsSupported", &osind); + if (err != EFI_SUCCESS) + return 0; + + return osind; +} + +#ifdef EFI_DEBUG +__attribute__((noinline)) void debug_break(void) { + /* 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. */ + volatile bool wait = true; + 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 + + +#ifdef EFI_DEBUG +void hexdump(const char16_t *prefix, const void *data, UINTN size) { + static const char hex[16] = "0123456789abcdef"; + _cleanup_free_ char16_t *buf = NULL; + const uint8_t *d = data; + + assert(prefix); + assert(data || size == 0); + + /* Debugging helper — please keep this around, even if not used */ + + buf = xnew(char16_t, size*2+1); + + for (UINTN i = 0; i < size; i++) { + buf[i*2] = hex[d[i] >> 4]; + buf[i*2+1] = hex[d[i] & 0x0F]; + } + + buf[size*2] = 0; + + log_error_stall(L"%s[%" PRIuN "]: %s", prefix, size, buf); +} +#endif + +#if defined(__i386__) || defined(__x86_64__) +static inline uint8_t inb(uint16_t port) { + uint8_t value; + asm volatile("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline void outb(uint16_t port, uint8_t value) { + asm volatile("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +void beep(UINTN 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, &FileSystemProtocol, (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; +} + +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, &DevicePathProtocol, (void **) &dp); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *end_node = dp; + while (!IsDevicePathEnd(end_node)) + end_node = NextDevicePathNode(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 + SIZE_OF_FILEPATH_DEVICE_PATH + END_DEVICE_PATH_LENGTH); + dp = mempcpy(*ret_dp, dp, dp_size); + + /* Replace end node with file media device path. Use memcpy() in case dp is unaligned (if accessed as + * FILEPATH_DEVICE_PATH). */ + dp->Type = MEDIA_DEVICE_PATH; + dp->SubType = MEDIA_FILEPATH_DP; + memcpy((uint8_t *) dp + offsetof(FILEPATH_DEVICE_PATH, PathName), file, file_size); + SetDevicePathNodeLength(dp, offsetof(FILEPATH_DEVICE_PATH, PathName) + file_size); + + dp = NextDevicePathNode(dp); + SetDevicePathEndNode(dp); + return EFI_SUCCESS; +} + +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(&(EFI_GUID) EFI_DEVICE_PATH_TO_TEXT_PROTOCOL_GUID, NULL, (void **) &dp_to_text); + if (err != EFI_SUCCESS) { + /* If the device path to text protocol is not available we can still do a best-effort attempt + * to convert it ourselves if we are given filepath-only device path. */ + + size_t size = 0; + for (const EFI_DEVICE_PATH *node = dp; !IsDevicePathEnd(node); + node = NextDevicePathNode(node)) { + + if (DevicePathType(node) != MEDIA_DEVICE_PATH || + DevicePathSubType(node) != MEDIA_FILEPATH_DP) + return err; + + size_t path_size = DevicePathNodeLength(node); + if (path_size <= offsetof(FILEPATH_DEVICE_PATH, PathName) || path_size % sizeof(char16_t)) + return EFI_INVALID_PARAMETER; + path_size -= offsetof(FILEPATH_DEVICE_PATH, PathName); + + _cleanup_free_ char16_t *old = str; + str = xmalloc(size + path_size); + if (old) { + memcpy(str, old, size); + str[size / sizeof(char16_t) - 1] = '\\'; + } + + memcpy(str + (size / sizeof(char16_t)), + ((uint8_t *) node) + offsetof(FILEPATH_DEVICE_PATH, PathName), + path_size); + size += path_size; + } + + *ret = TAKE_PTR(str); + 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; +} + +#if defined(__i386__) || defined(__x86_64__) +bool in_hypervisor(void) { + uint32_t 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; + + return !!(ecx & 0x80000000U); +} +#endif diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h new file mode 100644 index 0000000..bcab0df --- /dev/null +++ b/src/boot/efi/util.h @@ -0,0 +1,220 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <efilib.h> +#include <stddef.h> + +#include "string-util-fundamental.h" + +#define UINTN_MAX (~(UINTN)0) +#define INTN_MAX ((INTN)(UINTN_MAX>>1)) + +/* gnu-efi format specifiers for integers are fixed to either 64bit with 'l' and 32bit without a size prefix. + * We rely on %u/%d/%x to format regular ints, so ensure the size is what we expect. At the same time, we also + * need specifiers for (U)INTN which are native (pointer) sized. */ +assert_cc(sizeof(int) == sizeof(uint32_t)); +#if __SIZEOF_POINTER__ == 4 +# define PRIuN L"u" +# define PRIiN L"d" +#elif __SIZEOF_POINTER__ == 8 +# define PRIuN L"lu" +# define PRIiN L"ld" +#else +# error "Unexpected pointer size" +#endif + +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_ +static inline void *xmalloc(size_t size) { + void *p; + assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS); + return p; +} + +_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 *r = xmalloc(new_size); + new_size = MIN(old_size, new_size); + if (new_size > 0) + memcpy(r, p, new_size); + free(p); + return r; +} + +#define xpool_print(fmt, ...) ((char16_t *) ASSERT_SE_PTR(PoolPrint((fmt), ##__VA_ARGS__))) +#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 parse_boolean(const char *v, bool *b); + +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, UINTN size, uint32_t flags); +EFI_STATUS efivar_set_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN 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_get(const EFI_GUID *vendor, const char16_t *name, char16_t **value); +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, char **buffer, UINTN *size); +EFI_STATUS efivar_get_uint_string(const EFI_GUID *vendor, const char16_t *name, UINTN *i); +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); +void mangle_stub_cmdline(char16_t *cmdline); + +EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, UINTN off, UINTN size, char **content, UINTN *content_size); +EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf); + +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\<vendor>-<revision>.conf convention and the + * associated EFI variables. + */ +#define LOADER_GUID \ + &(const EFI_GUID) { 0x4a67b082, 0x0a4c, 0x41cf, { 0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f } } +#define EFI_GLOBAL_GUID &(const EFI_GUID) EFI_GLOBAL_VARIABLE + +void log_error_stall(const char16_t *fmt, ...); +EFI_STATUS log_oom(void); + +/* This works just like log_error_errno() from userspace, but requires you + * to provide err a second time if you want to use %r in the message! */ +#define log_error_status_stall(err, fmt, ...) \ + ({ \ + log_error_stall(fmt, ##__VA_ARGS__); \ + err; \ + }) + +void print_at(UINTN x, UINTN y, UINTN attr, const char16_t *str); +void clear_screen(UINTN attr); + +typedef int (*compare_pointer_func_t)(const void *a, const void *b); +void sort_pointer_array(void **array, UINTN n_members, compare_pointer_func_t compare); + +EFI_STATUS get_file_info_harder(EFI_FILE *handle, EFI_FILE_INFO **ret, UINTN *ret_size); + +EFI_STATUS readdir_harder(EFI_FILE *handle, EFI_FILE_INFO **buffer, UINTN *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 64bit, even on + * 32bit 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 32bit systems the address might not be convertible (as pointers are 32bit but + * EFI_PHYSICAL_ADDRESS 64bit) */ + assert(addr <= UINTPTR_MAX); + return (void *) (uintptr_t) addr; +} + +uint64_t get_os_indications_supported(void); + +#ifdef EFI_DEBUG +void debug_break(void); +extern uint8_t _text, _data; +/* Report the relocated position of text and data sections so that a debugger + * can attach to us. See debug-sd-boot.sh for how this can be done. */ +# define debug_hook(identity) Print(identity L"@0x%lx,0x%lx\n", POINTER_TO_PHYSICAL_ADDRESS(&_text), POINTER_TO_PHYSICAL_ADDRESS(&_data)) +#else +# define debug_hook(identity) +#endif + +#ifdef EFI_DEBUG +void hexdump(const char16_t *prefix, const void *data, UINTN size); +#endif + +#if defined(__i386__) || defined(__x86_64__) +void beep(UINTN beep_count); +#else +static inline void beep(UINTN beep_count) {} +#endif + +EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file); +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); + +#if defined(__i386__) || defined(__x86_64__) +bool in_hypervisor(void); +#else +static inline bool in_hypervisor(void) { + return false; +} +#endif diff --git a/src/boot/efi/vmm.c b/src/boot/efi/vmm.c new file mode 100644 index 0000000..f840705 --- /dev/null +++ b/src/boot/efi/vmm.c @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efilib.h> +#include <stdbool.h> + +#include "drivers.h" +#include "efi-string.h" +#include "string-util-fundamental.h" +#include "util.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, &DevicePathProtocol, (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, &(EFI_GUID)QEMU_KERNEL_LOADER_FS_MEDIA_GUID, 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; +} diff --git a/src/boot/efi/vmm.h b/src/boot/efi/vmm.h new file mode 100644 index 0000000..c8eb84f --- /dev/null +++ b/src/boot/efi/vmm.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> +#include <efilib.h> + +bool is_direct_boot(EFI_HANDLE device); diff --git a/src/boot/efi/xbootldr.c b/src/boot/efi/xbootldr.c new file mode 100644 index 0000000..7fef909 --- /dev/null +++ b/src/boot/efi/xbootldr.c @@ -0,0 +1,285 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <efi.h> +#include <efigpt.h> +#include <efilib.h> + +#include "util.h" +#include "xbootldr.h" + +union GptHeaderBuffer { + EFI_PARTITION_TABLE_HEADER gpt_header; + uint8_t space[CONST_ALIGN_TO(sizeof(EFI_PARTITION_TABLE_HEADER), 512)]; +}; + +static EFI_DEVICE_PATH *path_replace_hd( + const EFI_DEVICE_PATH *path, + const EFI_DEVICE_PATH *node, + const HARDDRIVE_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, new_node_len = 0; + if (new_node) + new_node_len = DevicePathNodeLength(&new_node->Header); + + EFI_DEVICE_PATH *ret = xmalloc(len + new_node_len + END_DEVICE_PATH_LENGTH); + EFI_DEVICE_PATH *end = mempcpy(ret, path, len); + + if (new_node) + end = mempcpy(end, new_node, new_node_len); + + SetDevicePathEndNode(end); + return ret; +} + +static bool verify_gpt(union GptHeaderBuffer *gpt_header_buffer, EFI_LBA lba_expected) { + EFI_PARTITION_TABLE_HEADER *h; + uint32_t crc32, crc32_saved; + EFI_STATUS err; + + assert(gpt_header_buffer); + + h = &gpt_header_buffer->gpt_header; + + /* 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(gpt_header_buffer, 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 > UINTN_MAX / h->NumberOfPartitionEntries) + return false; + + return true; +} + +static EFI_STATUS try_gpt( + 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; + union GptHeaderBuffer gpt; + EFI_STATUS err; + uint32_t crc32; + UINTN 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.gpt_header.AlternateLBA; + + if (!verify_gpt(&gpt, lba)) + return EFI_NOT_FOUND; + + /* Now load the GPT entry table */ + size = ALIGN_TO((UINTN) gpt.gpt_header.SizeOfPartitionEntry * (UINTN) gpt.gpt_header.NumberOfPartitionEntries, 512); + entries = xmalloc(size); + + err = block_io->ReadBlocks( + block_io, + block_io->Media->MediaId, + gpt.gpt_header.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.gpt_header.PartitionEntryArrayCRC32) + return EFI_CRC_ERROR; + + /* Now we can finally look for xbootloader partitions. */ + for (UINTN i = 0; i < gpt.gpt_header.NumberOfPartitionEntries; i++) { + EFI_PARTITION_ENTRY *entry = + (EFI_PARTITION_ENTRY *) ((uint8_t *) entries + gpt.gpt_header.SizeOfPartitionEntry * i); + + if (memcmp(&entry->PartitionTypeGUID, XBOOTLDR_GUID, sizeof(entry->PartitionTypeGUID)) != 0) + continue; + + if (entry->EndingLBA < entry->StartingLBA) /* Bogus? */ + continue; + + *ret_hd = (HARDDRIVE_DEVICE_PATH) { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_HARDDRIVE_DP, + }, + .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)); + + /* HARDDRIVE_DEVICE_PATH has padding, which at least OVMF does not like. */ + SetDevicePathNodeLength( + &ret_hd->Header, + offsetof(HARDDRIVE_DEVICE_PATH, SignatureType) + sizeof(ret_hd->SignatureType)); + + 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(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, &DevicePathProtocol, (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; !IsDevicePathEnd(node); node = NextDevicePathNode(node)) { + if (DevicePathType(node) != MEDIA_DEVICE_PATH) + continue; + + if (DevicePathSubType(node) != 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 = path_replace_hd(partition_path, part_node, NULL); + + EFI_HANDLE disk_handle; + EFI_BLOCK_IO_PROTOCOL *block_io; + err = BS->LocateDevicePath(&BlockIoProtocol, &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, &BlockIoProtocol, (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 (UINTN 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( + 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 = path_replace_hd(partition_path, part_node, &hd); + return EFI_SUCCESS; + } + + /* No xbootloader partition found */ + return EFI_NOT_FOUND; +} + +EFI_STATUS xbootldr_open(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(device); + assert(ret_device); + assert(ret_root_dir); + + err = find_device(device, &partition_path); + if (err != EFI_SUCCESS) + return err; + + EFI_DEVICE_PATH *dp = partition_path; + err = BS->LocateDevicePath(&BlockIoProtocol, &dp, &new_device); + if (err != EFI_SUCCESS) + return err; + + err = open_volume(new_device, &root_dir); + if (err != EFI_SUCCESS) + return err; + + *ret_device = new_device; + *ret_root_dir = root_dir; + return EFI_SUCCESS; +} diff --git a/src/boot/efi/xbootldr.h b/src/boot/efi/xbootldr.h new file mode 100644 index 0000000..205ce71 --- /dev/null +++ b/src/boot/efi/xbootldr.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <efi.h> + +#define XBOOTLDR_GUID \ + &(const EFI_GUID) { 0xbc13c2ff, 0x59e6, 0x4262, { 0xa3, 0x52, 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72 } } + +EFI_STATUS xbootldr_open(EFI_HANDLE *device, EFI_HANDLE *ret_device, EFI_FILE **ret_root_dir); |