diff options
Diffstat (limited to 'src/boot/efi/stub.c')
-rw-r--r-- | src/boot/efi/stub.c | 415 |
1 files changed, 415 insertions, 0 deletions
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; +} |