From 70b7e03ebbd90046d23423571ab0440ba8e2047f Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Mon, 11 Aug 2025 15:33:35 +0100 Subject: [PATCH] sd-stub: use memory proto if available and set kernel memory to RX with NX_COMPAT When NX_COMPAT gets enabled, firmwares will enforce that executable memory is either writable or executable. This needs kernel compatibility, when it will happen the kernel will have the NX_COMPAT bit set. If it is, set the memory buffer to RO. Note that this must be undone on failure, as EDK2 in some configurations overwrites memory ranges that are returned with FreePages() with a fixed pattern, so if the pages are RO it will crash. This is only an issue with the new custom PE loader, as LoadImage() and StartImage() will always do the right thing automatically. https://microsoft.github.io/mu/WhatAndWhy/enhancedmemoryprotection/ https://www.kraxel.org/blog/2023/12/uefi-nx-linux-boot/ Follow-up for cab9c7b5a42effa8a45611fc6b8556138c869b5f Fixes https://github.com/systemd/systemd/issues/38545 --- src/boot/linux.c | 75 +++++++++++++++++++++++++++++++ src/boot/pe.c | 13 ++++++ src/boot/pe.h | 6 +++ src/boot/proto/memory-attribute.h | 31 +++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 src/boot/proto/memory-attribute.h diff --git a/src/boot/linux.c b/src/boot/linux.c index 38576716cdc..f18d91e9e42 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 8f384630417..289e69710c1 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 dc4088c948c..878d98de702 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 00000000000..7b9360818c1 --- /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; -- 2.47.3