diff --git a/src/boot/boot.c b/src/boot/boot.c index 925cc0c41d2..d1a3850f4af 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -2688,7 +2688,8 @@ static EFI_STATUS call_image_start( if (err == EFI_UNSUPPORTED && entry->type == LOADER_LINUX) { uint32_t compat_address; - err = pe_kernel_info(loaded_image->ImageBase, &compat_address, /* ret_size_in_memory= */ NULL); + err = pe_kernel_info(loaded_image->ImageBase, /* ret_entry_point= */ NULL, &compat_address, + /* ret_image_base= */ NULL, /* ret_size_in_memory= */ NULL); if (err != EFI_SUCCESS) { if (err != EFI_UNSUPPORTED) return log_error_status(err, "Error finding kernel compat entry address: %m"); diff --git a/src/boot/device-path-util.c b/src/boot/device-path-util.c index 2764373d94a..d8c8e5780d4 100644 --- a/src/boot/device-path-util.c +++ b/src/boot/device-path-util.c @@ -5,6 +5,12 @@ #include "string-util-fundamental.h" #include "util.h" +static const EFI_DEVICE_PATH *device_path_find_end_node(const EFI_DEVICE_PATH *dp) { + while (!device_path_is_end(dp)) + dp = device_path_next_node(dp); + return dp; +} + 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; @@ -16,9 +22,7 @@ EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DE if (err != EFI_SUCCESS) return err; - EFI_DEVICE_PATH *end_node = dp; - while (!device_path_is_end(end_node)) - end_node = device_path_next_node(end_node); + const EFI_DEVICE_PATH *end_node = device_path_find_end_node(dp); size_t file_size = strsize16(file); size_t dp_size = (uint8_t *) end_node - (uint8_t *) dp; @@ -158,7 +162,9 @@ EFI_DEVICE_PATH *device_path_replace_node( * node. If new_node is provided, it is appended at the end of the new path. */ assert(path); - assert(node); + + if (!node) + node = device_path_find_end_node(path); size_t len = (uint8_t *) node - (uint8_t *) path; EFI_DEVICE_PATH *ret = xmalloc(len + (new_node ? new_node->Length : 0) + sizeof(EFI_DEVICE_PATH)); diff --git a/src/boot/linux.c b/src/boot/linux.c index 00bdf8c1ce4..b2c5200f1b3 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -8,6 +8,7 @@ * This method works for Linux 5.8 and newer on ARM/Aarch64, x86/x68_64 and RISC-V. */ +#include "device-path-util.h" #include "efi-log.h" #include "initrd.h" #include "linux.h" @@ -15,82 +16,12 @@ #include "proto/device-path.h" #include "proto/loaded-image.h" #include "secure-boot.h" +#include "shim.h" #include "util.h" #define STUB_PAYLOAD_GUID \ { 0x55c5d1f8, 0x04cd, 0x46b5, { 0x8a, 0x20, 0xe5, 0x6c, 0xbb, 0x30, 0x52, 0xd0 } } -typedef struct { - const void *addr; - size_t len; - const EFI_DEVICE_PATH *device_path; -} ValidationContext; - -static bool validate_payload( - const void *ctx, const EFI_DEVICE_PATH *device_path, const void *file_buffer, size_t file_size) { - - const ValidationContext *payload = ASSERT_PTR(ctx); - - if (device_path != payload->device_path) - return false; - - /* Security arch (1) protocol does not provide a file buffer. Instead we are supposed to fetch the payload - * ourselves, which is not needed as we already have everything in memory and the device paths match. */ - if (file_buffer && (file_buffer != payload->addr || file_size != payload->len)) - return false; - - return true; -} - -static EFI_STATUS load_image(EFI_HANDLE parent, const void *source, size_t len, EFI_HANDLE *ret_image) { - assert(parent); - assert(source); - assert(ret_image); - - /* We could pass a NULL device path, but it's nicer to provide something and it allows us to identify - * the loaded image from within the security hooks. */ - struct { - VENDOR_DEVICE_PATH payload; - EFI_DEVICE_PATH end; - } _packed_ payload_device_path = { - .payload = { - .Header = { - .Type = MEDIA_DEVICE_PATH, - .SubType = MEDIA_VENDOR_DP, - .Length = sizeof(payload_device_path.payload), - }, - .Guid = STUB_PAYLOAD_GUID, - }, - .end = { - .Type = END_DEVICE_PATH_TYPE, - .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, - .Length = sizeof(payload_device_path.end), - }, - }; - - /* We want to support unsigned kernel images as payload, which is safe to do under secure boot - * because it is embedded in this stub loader (and since it is already running it must be trusted). */ - install_security_override( - validate_payload, - &(ValidationContext) { - .addr = source, - .len = len, - .device_path = &payload_device_path.payload.Header, - }); - - EFI_STATUS ret = BS->LoadImage( - /*BootPolicy=*/false, - parent, - &payload_device_path.payload.Header, - (void *) source, - len, - ret_image); - - uninstall_security_override(); - - return ret; -} - EFI_STATUS linux_exec( EFI_HANDLE parent, const char16_t *cmdline, @@ -98,14 +29,15 @@ EFI_STATUS linux_exec( const struct iovec *initrd) { size_t kernel_size_in_memory = 0; - uint32_t compat_address; + uint32_t compat_entry_point, entry_point; + uint64_t image_base; EFI_STATUS err; assert(parent); assert(iovec_is_set(kernel)); assert(iovec_is_valid(initrd)); - err = pe_kernel_info(kernel->iov_base, &compat_address, &kernel_size_in_memory); + err = pe_kernel_info(kernel->iov_base, &entry_point, &compat_entry_point, &image_base, &kernel_size_in_memory); #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 @@ -120,16 +52,69 @@ EFI_STATUS linux_exec( if (err != EFI_SUCCESS) return log_error_status(err, "Bad kernel image: %m"); - _cleanup_(unload_imagep) EFI_HANDLE kernel_image = NULL; - err = load_image(parent, kernel->iov_base, kernel->iov_len, &kernel_image); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error loading kernel image: %m"); - - EFI_LOADED_IMAGE_PROTOCOL *loaded_image; + EFI_LOADED_IMAGE_PROTOCOL* parent_loaded_image; err = BS->HandleProtocol( - kernel_image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); + parent, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &parent_loaded_image); if (err != EFI_SUCCESS) - return log_error_status(err, "Error getting kernel loaded image protocol: %m"); + return log_error_status(err, "Cannot get parent loaded image: %m"); + + err = pe_kernel_check_no_relocation(kernel->iov_base); + if (err != EFI_SUCCESS) + return err; + + const PeSectionHeader *headers; + size_t n_headers; + + /* Do we need to validate anyting here? the len? */ + err = pe_section_table_from_base(kernel->iov_base, &headers, &n_headers); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot read sections: %m"); + + /* Do we need to ensure under 4gb address on x86? */ + _cleanup_pages_ Pages loaded_kernel_pages = xmalloc_pages( + AllocateAnyPages, EfiLoaderCode, EFI_SIZE_TO_PAGES(kernel_size_in_memory), 0); + + uint8_t* loaded_kernel = PHYSICAL_ADDRESS_TO_POINTER(loaded_kernel_pages.addr); + FOREACH_ARRAY(h, headers, n_headers) { + if (h->PointerToRelocations != 0) + return log_error_status(EFI_LOAD_ERROR, "Inner kernel image contains sections with relocations, which we do not support."); + if (h->SizeOfRawData == 0) + continue; + + if ((h->VirtualAddress < image_base) + || (h->VirtualAddress - image_base + h->SizeOfRawData > kernel_size_in_memory)) + return log_error_status(EFI_LOAD_ERROR, "Section would write outside of memory"); + memcpy(loaded_kernel + h->VirtualAddress - image_base, + (const uint8_t*)kernel->iov_base + h->PointerToRawData, + h->SizeOfRawData); + memzero(loaded_kernel + h->VirtualAddress + h->SizeOfRawData, + h->VirtualSize - h->SizeOfRawData); + } + + _cleanup_free_ EFI_LOADED_IMAGE_PROTOCOL* loaded_image = xnew(EFI_LOADED_IMAGE_PROTOCOL, 1); + + VENDOR_DEVICE_PATH device_node = { + .Header = { + .Type = MEDIA_DEVICE_PATH, + .SubType = MEDIA_VENDOR_DP, + .Length = sizeof(device_node), + }, + .Guid = STUB_PAYLOAD_GUID, + }; + + _cleanup_free_ EFI_DEVICE_PATH* file_path = device_path_replace_node(parent_loaded_image->FilePath, NULL, &device_node.Header); + + *loaded_image = (EFI_LOADED_IMAGE_PROTOCOL) { + .Revision = 0x1000, + .ParentHandle = parent, + .SystemTable = ST, + .DeviceHandle = parent_loaded_image->DeviceHandle, + .FilePath = file_path, + .ImageBase = loaded_kernel, + .ImageSize = kernel_size_in_memory, + .ImageCodeType = /*EFI_LOADER_CODE*/1, + .ImageDataType = /*EFI_LOADER_DATA*/2, + }; if (cmdline) { loaded_image->LoadOptions = (void *) cmdline; @@ -141,15 +126,32 @@ EFI_STATUS linux_exec( if (err != EFI_SUCCESS) return log_error_status(err, "Error registering initrd: %m"); - log_wait(); - err = BS->StartImage(kernel_image, NULL, NULL); + EFI_HANDLE kernel_image = NULL; - /* Try calling the kernel compat entry point if one exists. */ - if (err == EFI_UNSUPPORTED && compat_address > 0) { + err = BS->InstallMultipleProtocolInterfaces( + &kernel_image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), loaded_image, + NULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot install loaded image protocol: %m"); + + log_wait(); + + if (entry_point > 0) { + EFI_IMAGE_ENTRY_POINT entry = + (EFI_IMAGE_ENTRY_POINT) ((const uint8_t *) loaded_image->ImageBase + entry_point); + err = entry(kernel_image, ST); + } else if (compat_entry_point > 0) { + /* Try calling the kernel compat entry point if one exists. */ EFI_IMAGE_ENTRY_POINT compat_entry = - (EFI_IMAGE_ENTRY_POINT) ((uint8_t *) loaded_image->ImageBase + compat_address); + (EFI_IMAGE_ENTRY_POINT) ((const uint8_t *) loaded_image->ImageBase + compat_entry_point); err = compat_entry(kernel_image, ST); } + EFI_STATUS uninstall_err = BS->UninstallMultipleProtocolInterfaces( + kernel_image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), loaded_image, + NULL); + if (uninstall_err != EFI_SUCCESS) + return log_error_status(uninstall_err, "Cannot uninstall loaded image protocol: %m"); + return log_error_status(err, "Error starting kernel image: %m"); } diff --git a/src/boot/pe.c b/src/boot/pe.c index 9ce978a48b7..beab0eb670a 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -70,6 +70,13 @@ typedef struct CoffFileHeader { #define OPTHDR32_MAGIC 0x10B /* PE32 OptionalHeader */ #define OPTHDR64_MAGIC 0x20B /* PE32+ OptionalHeader */ +typedef struct PeImageDataDirectory { + uint32_t VirtualAddress; + uint32_t Size; +} _packed_ PeImageDataDirectory; + +#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 + typedef struct PeOptionalHeader { uint16_t Magic; uint8_t LinkerMajor; @@ -100,7 +107,28 @@ typedef struct PeOptionalHeader { uint32_t CheckSum; uint16_t Subsystem; uint16_t DllCharacteristics; - /* fields with different sizes for 32/64 omitted */ + union { + struct { + uint64_t SizeOfStackReserve64; + uint64_t SizeOfStackCommit64; + uint64_t SizeOfHeapReserve64; + uint64_t SizeOfHeapCommit64; + uint32_t LoaderFlags64; + uint32_t NumberOfRvaAndSizes64; + + PeImageDataDirectory DataDirectory64[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; + }; + struct { + uint32_t SizeOfStackReserve32; + uint32_t SizeOfStackCommit32; + uint32_t SizeOfHeapReserve32; + uint32_t SizeOfHeapCommit32; + uint32_t LoaderFlags32; + uint32_t NumberOfRvaAndSizes32; + + PeImageDataDirectory DataDirectory32[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; + }; + }; } _packed_ PeOptionalHeader; typedef struct PeFileHeader { @@ -430,9 +458,8 @@ static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const return 0; } -EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address, size_t *ret_size_in_memory) { +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_entry_point, uint32_t *ret_compat_entry_point, uint64_t *ret_image_base, size_t *ret_size_in_memory) { assert(base); - assert(ret_compat_address); const DosFileHeader *dos = (const DosFileHeader *) base; if (!verify_dos(dos)) @@ -442,26 +469,89 @@ EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address, size_t if (!verify_pe(dos, pe, /* allow_compatibility= */ true)) return EFI_LOAD_ERROR; + uint64_t image_base; + switch (pe->OptionalHeader.Magic) { + case OPTHDR32_MAGIC: + image_base = pe->OptionalHeader.ImageBase32; + break; + case OPTHDR64_MAGIC: + image_base = pe->OptionalHeader.ImageBase64; + break; + default: + assert_not_reached(); + } + /* When allocating we need to also consider the virtual/uninitialized data sections, so parse it out * of the SizeOfImage field in the PE header and return it */ - if (ret_size_in_memory) - *ret_size_in_memory = pe->OptionalHeader.SizeOfImage; + size_t size_in_memory = pe->OptionalHeader.SizeOfImage; /* 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; + if (ret_entry_point) + *ret_entry_point = pe->OptionalHeader.AddressOfEntryPoint; + if (ret_compat_entry_point) + *ret_compat_entry_point = 0; + if (ret_image_base) + *ret_image_base = image_base; + if (ret_size_in_memory) + *ret_size_in_memory = size_in_memory; return EFI_SUCCESS; } - uint32_t compat_address = get_compatibility_entry_address(dos, pe); - if (compat_address == 0) + uint32_t compat_entry_point = get_compatibility_entry_address(dos, pe); + if (compat_entry_point == 0) /* Image type not supported and no compat entry found. */ return EFI_UNSUPPORTED; - *ret_compat_address = compat_address; + if (ret_entry_point) + *ret_entry_point = 0; + if (ret_compat_entry_point) + *ret_compat_entry_point = compat_entry_point; + if (ret_image_base) + *ret_image_base = image_base; + if (ret_size_in_memory) + *ret_size_in_memory = size_in_memory; + + return EFI_SUCCESS; +} + +/* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only */ +#define BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY 5 + +/* We do not expect PE inner kernels to have any relocations. However that might be wrong for some + * architectures, or it might change in the future. If the case of relocation arise, we should transform this + * function in a function applying the relocations. However for now, since it would not be exercised and + * would bitrot, we leave it as a check that relocations are never expected. + */ +EFI_STATUS pe_kernel_check_no_relocation(const void *base) { + assert(base); + + const DosFileHeader *dos = base; + if (!verify_dos(dos)) + return EFI_LOAD_ERROR; + + const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader); + if (!verify_pe(dos, pe, /* allow_compatibility= */ true)) + return EFI_LOAD_ERROR; + + const PeImageDataDirectory *data_directory; + switch (pe->OptionalHeader.Magic) { + case OPTHDR32_MAGIC: + data_directory = pe->OptionalHeader.DataDirectory32; + break; + case OPTHDR64_MAGIC: + data_directory = pe->OptionalHeader.DataDirectory64; + break; + default: + assert_not_reached(); + } + + if (data_directory[BASE_RELOCATION_TABLE_DATA_DIRECTORY_ENTRY].Size != 0) + return log_error_status(EFI_LOAD_ERROR, "Inner kernel image contains base relocations, which we do not support."); + return EFI_SUCCESS; } diff --git a/src/boot/pe.h b/src/boot/pe.h index a2847de8827..dc4088c948c 100644 --- a/src/boot/pe.h +++ b/src/boot/pe.h @@ -53,4 +53,6 @@ EFI_STATUS pe_memory_locate_sections( const char *const section_names[], PeSectionVector sections[]); -EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address, size_t *ret_size_in_memory); +EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_entry_point, uint32_t *ret_compat_entry_point, uint64_t *ret_image_base, size_t *ret_size_in_memory); + +EFI_STATUS pe_kernel_check_no_relocation(const void *base);