]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-stub: use memory proto if available and set kernel memory to RX with NX_COMPAT
authorLuca Boccassi <luca.boccassi@gmail.com>
Mon, 11 Aug 2025 14:33:35 +0000 (15:33 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 19 Aug 2025 15:16:57 +0000 (17:16 +0200)
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
src/boot/pe.c
src/boot/pe.h
src/boot/proto/memory-attribute.h [new file with mode: 0644]

index 38576716cdcc82c2419e6e8472bfb71f81e32a94..f18d91e9e42622aedc959efe5d3ed5f31fb02a4c 100644 (file)
@@ -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");
 }
index 8f3846304177726f95f578426622032e334b06a7..289e69710c1a6cb57b02f4083e7b474001debcb9 100644 (file)
@@ -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,
index dc4088c948c296b05f353b006d10def98c6c3195..878d98de70207893eaab72d51ab496721362d97a 100644 (file)
@@ -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 (file)
index 0000000..7b93608
--- /dev/null
@@ -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;