diff --git a/src/boot/linux.c b/src/boot/linux.c index 38576716cd..f18d91e9e4 100644 --- a/src/boot/linux.c +++ b/src/boot/linux.c @@ -15,6 +15,7 @@ #include "pe.h" #include "proto/device-path.h" #include "proto/loaded-image.h" +#include "proto/memory-attribute.h" #include "secure-boot.h" #include "shim.h" #include "util.h" @@ -125,6 +126,50 @@ static EFI_STATUS load_via_boot_services( return log_error_status(err, "Error starting kernel image with shim: %m"); } +static EFI_STATUS kernel_set_nx(EFI_PHYSICAL_ADDRESS addr, uint64_t length) { + EFI_MEMORY_ATTRIBUTE_PROTOCOL *memory_proto; + EFI_STATUS err; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_MEMORY_ATTRIBUTE_PROTOCOL), NULL, (void **) &memory_proto); + if (err != EFI_SUCCESS) { + log_debug("No EFI_MEMORY_ATTRIBUTE_PROTOCOL found, skipping NX_COMPAT support."); + return EFI_SUCCESS; /* ignore if firmware lacks support */ + } + + err = memory_proto->SetMemoryAttributes(memory_proto, addr, length, EFI_MEMORY_RO); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot make kernel image read-only: %m"); + + err = memory_proto->ClearMemoryAttributes(memory_proto, addr, length, EFI_MEMORY_XP); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot make kernel image executable: %m"); + + log_debug("Changed kernel image to read-only for NX_COMPAT support."); + + return EFI_SUCCESS; +} + +static EFI_STATUS kernel_clear_nx(EFI_PHYSICAL_ADDRESS addr, uint64_t length) { + EFI_MEMORY_ATTRIBUTE_PROTOCOL *memory_proto; + EFI_STATUS err; + + err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_MEMORY_ATTRIBUTE_PROTOCOL), NULL, (void **) &memory_proto); + if (err != EFI_SUCCESS) { + log_debug("No EFI_MEMORY_ATTRIBUTE_PROTOCOL found, skipping NX_COMPAT support."); + return EFI_SUCCESS; /* ignore if firmware lacks support */ + } + + err = memory_proto->SetMemoryAttributes(memory_proto, addr, length, EFI_MEMORY_XP); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot make kernel image non-executable: %m"); + + err = memory_proto->ClearMemoryAttributes(memory_proto, addr, length, EFI_MEMORY_RO); + if (err != EFI_SUCCESS) + return log_error_status(err, "Cannot make kernel image writable: %m"); + + return EFI_SUCCESS; +} + EFI_STATUS linux_exec( EFI_HANDLE parent_image, const char16_t *cmdline, @@ -198,6 +243,16 @@ EFI_STATUS linux_exec( if (err != EFI_SUCCESS) return err; + /* As per MSFT requirement, memory pages need to be marked W^X. + * Firmwares will start enforcing this at some point in the near-ish future. + * The kernel needs to mark this as supported explicitly, otherwise it will crash. + * https://microsoft.github.io/mu/WhatAndWhy/enhancedmemoryprotection/ + * https://www.kraxel.org/blog/2023/12/uefi-nx-linux-boot/ */ + _cleanup_free_ EFI_PHYSICAL_ADDRESS *nx_sections_addrs = NULL; + _cleanup_free_ uint64_t *nx_sections_lengths = NULL; + size_t nx_sections = 0; + bool nx_compat = pe_kernel_check_nx_compat(kernel->iov_base); + const PeSectionHeader *headers; size_t n_headers; @@ -225,6 +280,20 @@ EFI_STATUS linux_exec( h->SizeOfRawData); memzero(loaded_kernel + h->VirtualAddress + h->SizeOfRawData, h->VirtualSize - h->SizeOfRawData); + + /* Not a code section? Nothing to do, leave as-is. */ + if (nx_compat && ((h->Characteristics & PE_CODE) || (h->Characteristics & PE_EXECUTE))) { + nx_sections_addrs = xrealloc(nx_sections_addrs, nx_sections * sizeof(EFI_PHYSICAL_ADDRESS), (nx_sections + 1) * sizeof(EFI_PHYSICAL_ADDRESS)); + nx_sections_lengths = xrealloc(nx_sections_lengths, nx_sections * sizeof(uint64_t), (nx_sections + 1) * sizeof(uint64_t)); + nx_sections_addrs[nx_sections] = POINTER_TO_PHYSICAL_ADDRESS(loaded_kernel + h->VirtualAddress - image_base); + nx_sections_lengths[nx_sections] = h->VirtualSize; + + err = kernel_set_nx(nx_sections_addrs[nx_sections], nx_sections_lengths[nx_sections]); + if (err != EFI_SUCCESS) + return err; + + ++nx_sections; + } } _cleanup_free_ KERNEL_FILE_PATH *kernel_file_path = xnew(KERNEL_FILE_PATH, 1); @@ -273,5 +342,11 @@ EFI_STATUS linux_exec( err = compat_entry(parent_image, ST); } + /* On failure we'll free the buffers. EDK2 requires the memory buffers to be writable and + * non-executable, as in some configurations it will overwrite them with a fixed pattern, so if the + * attributes are not restored FreePages() will crash. */ + for (size_t i = 0; i < nx_sections; i++) + (void) kernel_clear_nx(nx_sections_addrs[i], nx_sections_lengths[i]); + return log_error_status(err, "Error starting kernel image: %m"); } diff --git a/src/boot/pe.c b/src/boot/pe.c index 8f38463041..289e69710c 100644 --- a/src/boot/pe.c +++ b/src/boot/pe.c @@ -9,6 +9,7 @@ #define DOS_FILE_MAGIC "MZ" #define PE_FILE_MAGIC "PE\0\0" +#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 #if defined(__i386__) # define TARGET_MACHINE_TYPE 0x014CU @@ -555,6 +556,18 @@ EFI_STATUS pe_kernel_check_no_relocation(const void *base) { return EFI_SUCCESS; } +bool pe_kernel_check_nx_compat(const void *base) { + const DosFileHeader *dos = ASSERT_PTR(base); + if (!verify_dos(dos)) + return false; + + const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader); + if (!verify_pe(dos, pe, /* allow_compatibility= */ true)) + return false; + + return pe->OptionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT; +} + EFI_STATUS pe_section_table_from_base( const void *base, const PeSectionHeader **ret_section_table, diff --git a/src/boot/pe.h b/src/boot/pe.h index dc4088c948..878d98de70 100644 --- a/src/boot/pe.h +++ b/src/boot/pe.h @@ -3,6 +3,10 @@ #include "efi.h" +/* PE flags in the Characteristics attribute of the optional header indicating executable code */ +#define PE_CODE 0x00000020 +#define PE_EXECUTE 0x20000000 + /* This is the actual PE format of the section header */ typedef struct PeSectionHeader { uint8_t Name[8]; @@ -56,3 +60,5 @@ EFI_STATUS pe_memory_locate_sections( 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); + +bool pe_kernel_check_nx_compat(const void *base); diff --git a/src/boot/proto/memory-attribute.h b/src/boot/proto/memory-attribute.h new file mode 100644 index 0000000000..7b9360818c --- /dev/null +++ b/src/boot/proto/memory-attribute.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +#define EFI_MEMORY_ATTRIBUTE_PROTOCOL_GUID \ + GUID_DEF(0xf4560cf6, 0x40ec, 0x4b4a, 0xa1, 0x92, 0xbf, 0x1d, 0x57, 0xd0, 0xb1, 0x89) + +#define EFI_MEMORY_RP 0x0000000000002000 +#define EFI_MEMORY_XP 0x0000000000004000 +#define EFI_MEMORY_RO 0x0000000000020000 + +struct _EFI_MEMORY_ATTRIBUTE_PROTOCOL; + +typedef struct _EFI_MEMORY_ATTRIBUTE_PROTOCOL { + EFI_STATUS (EFIAPI *GetMemoryAttributes)( + struct _EFI_MEMORY_ATTRIBUTE_PROTOCOL *This, + EFI_PHYSICAL_ADDRESS BaseAddress, + uint64_t Length, + uint64_t *Attributes); + EFI_STATUS (EFIAPI *SetMemoryAttributes)( + struct _EFI_MEMORY_ATTRIBUTE_PROTOCOL *This, + EFI_PHYSICAL_ADDRESS BaseAddress, + uint64_t Length, + uint64_t Attributes); + EFI_STATUS (EFIAPI *ClearMemoryAttributes)( + struct _EFI_MEMORY_ATTRIBUTE_PROTOCOL *This, + EFI_PHYSICAL_ADDRESS BaseAddress, + uint64_t Length, + uint64_t Attributes); +} EFI_MEMORY_ATTRIBUTE_PROTOCOL;