| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| /* |
| * x86 specific code to for EFI handover boot protocol |
| * Linux kernels version 5.8 and newer support providing the initrd by |
| * LINUX_INITRD_MEDIA_GUID DevicePath. In order to support older kernels too, |
| * this x86 specific linux_exec function passes the initrd by setting the |
| * corresponding fields in the setup_header struct. |
| * |
| * see https://docs.kernel.org/x86/boot.html |
| */ |
| |
| #include <efi.h> |
| #include <efilib.h> |
| |
| #include "initrd.h" |
| #include "linux.h" |
| #include "macro-fundamental.h" |
| #include "util.h" |
| |
| #define KERNEL_SECTOR_SIZE 512u |
| #define BOOT_FLAG_MAGIC 0xAA55u |
| #define SETUP_MAGIC 0x53726448u /* "HdrS" */ |
| #define SETUP_VERSION_2_11 0x20bu |
| #define SETUP_VERSION_2_12 0x20cu |
| #define SETUP_VERSION_2_15 0x20fu |
| #define CMDLINE_PTR_MAX 0xA0000u |
| |
| enum { |
| XLF_KERNEL_64 = 1 << 0, |
| XLF_CAN_BE_LOADED_ABOVE_4G = 1 << 1, |
| XLF_EFI_HANDOVER_32 = 1 << 2, |
| XLF_EFI_HANDOVER_64 = 1 << 3, |
| #ifdef __x86_64__ |
| XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_64, |
| #else |
| XLF_EFI_HANDOVER = XLF_EFI_HANDOVER_32, |
| #endif |
| }; |
| |
| typedef struct { |
| uint8_t setup_sects; |
| uint16_t root_flags; |
| uint32_t syssize; |
| uint16_t ram_size; |
| uint16_t vid_mode; |
| uint16_t root_dev; |
| uint16_t boot_flag; |
| uint8_t jump; /* We split the 2-byte jump field from the spec in two for convenience. */ |
| uint8_t setup_size; |
| uint32_t header; |
| uint16_t version; |
| uint32_t realmode_swtch; |
| uint16_t start_sys_seg; |
| uint16_t kernel_version; |
| uint8_t type_of_loader; |
| uint8_t loadflags; |
| uint16_t setup_move_size; |
| uint32_t code32_start; |
| uint32_t ramdisk_image; |
| uint32_t ramdisk_size; |
| uint32_t bootsect_kludge; |
| uint16_t heap_end_ptr; |
| uint8_t ext_loader_ver; |
| uint8_t ext_loader_type; |
| uint32_t cmd_line_ptr; |
| uint32_t initrd_addr_max; |
| uint32_t kernel_alignment; |
| uint8_t relocatable_kernel; |
| uint8_t min_alignment; |
| uint16_t xloadflags; |
| uint32_t cmdline_size; |
| uint32_t hardware_subarch; |
| uint64_t hardware_subarch_data; |
| uint32_t payload_offset; |
| uint32_t payload_length; |
| uint64_t setup_data; |
| uint64_t pref_address; |
| uint32_t init_size; |
| uint32_t handover_offset; |
| } _packed_ SetupHeader; |
| |
| /* We really only care about a few fields, but we still have to provide a full page otherwise. */ |
| typedef struct { |
| uint8_t pad[192]; |
| uint32_t ext_ramdisk_image; |
| uint32_t ext_ramdisk_size; |
| uint32_t ext_cmd_line_ptr; |
| uint8_t pad2[293]; |
| SetupHeader hdr; |
| uint8_t pad3[3480]; |
| } _packed_ BootParams; |
| assert_cc(offsetof(BootParams, ext_ramdisk_image) == 0x0C0); |
| assert_cc(sizeof(BootParams) == 4096); |
| |
| #ifdef __i386__ |
| # define __regparm0__ __attribute__((regparm(0))) |
| #else |
| # define __regparm0__ |
| #endif |
| |
| typedef void (*handover_f)(void *parent, EFI_SYSTEM_TABLE *table, BootParams *params) __regparm0__ |
| __attribute__((sysv_abi)); |
| |
| static void linux_efi_handover(EFI_HANDLE parent, uintptr_t kernel, BootParams *params) { |
| assert(params); |
| |
| kernel += (params->hdr.setup_sects + 1) * KERNEL_SECTOR_SIZE; /* 32bit entry address. */ |
| |
| /* Old kernels needs this set, while newer ones seem to ignore this. Note that this gets truncated on |
| * above 4G boots, which is fine as long as we do not use the value to jump to kernel entry. */ |
| params->hdr.code32_start = kernel; |
| |
| #ifdef __x86_64__ |
| kernel += KERNEL_SECTOR_SIZE; /* 64bit entry address. */ |
| #endif |
| |
| kernel += params->hdr.handover_offset; /* 32/64bit EFI handover address. */ |
| |
| /* Note in EFI mixed mode this now points to the correct 32bit handover entry point, allowing a 64bit |
| * kernel to be booted from a 32bit sd-stub. */ |
| |
| handover_f handover = (handover_f) kernel; |
| handover(parent, ST, params); |
| } |
| |
| EFI_STATUS linux_exec_efi_handover( |
| EFI_HANDLE parent, |
| const char16_t *cmdline, |
| const void *linux_buffer, |
| size_t linux_length, |
| const void *initrd_buffer, |
| size_t initrd_length) { |
| |
| assert(parent); |
| assert(linux_buffer); |
| assert(initrd_buffer || initrd_length == 0); |
| |
| if (linux_length < sizeof(BootParams)) |
| return EFI_LOAD_ERROR; |
| |
| const BootParams *image_params = (const BootParams *) linux_buffer; |
| if (image_params->hdr.header != SETUP_MAGIC || image_params->hdr.boot_flag != BOOT_FLAG_MAGIC) |
| return log_error_status(EFI_UNSUPPORTED, "Unsupported kernel image."); |
| if (image_params->hdr.version < SETUP_VERSION_2_11) |
| return log_error_status(EFI_UNSUPPORTED, "Kernel too old."); |
| if (!image_params->hdr.relocatable_kernel) |
| return log_error_status(EFI_UNSUPPORTED, "Kernel is not relocatable."); |
| |
| /* The xloadflags were added in version 2.12+ of the boot protocol but the handover support predates |
| * that, so we cannot safety-check this for 2.11. */ |
| if (image_params->hdr.version >= SETUP_VERSION_2_12 && |
| !FLAGS_SET(image_params->hdr.xloadflags, XLF_EFI_HANDOVER)) |
| return log_error_status(EFI_UNSUPPORTED, "Kernel does not support EFI handover protocol."); |
| |
| bool can_4g = image_params->hdr.version >= SETUP_VERSION_2_12 && |
| FLAGS_SET(image_params->hdr.xloadflags, XLF_CAN_BE_LOADED_ABOVE_4G); |
| |
| if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + linux_length > UINT32_MAX) |
| return log_error_status( |
| EFI_UNSUPPORTED, |
| "Unified kernel image was loaded above 4G, but kernel lacks support."); |
| if (!can_4g && POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) + initrd_length > UINT32_MAX) |
| return log_error_status(EFI_UNSUPPORTED, "Initrd is above 4G, but kernel lacks support."); |
| |
| _cleanup_pages_ Pages boot_params_page = xmalloc_pages( |
| can_4g ? AllocateAnyPages : AllocateMaxAddress, |
| EfiLoaderData, |
| EFI_SIZE_TO_PAGES(sizeof(BootParams)), |
| UINT32_MAX /* Below the 4G boundary */); |
| BootParams *boot_params = PHYSICAL_ADDRESS_TO_POINTER(boot_params_page.addr); |
| *boot_params = (BootParams){}; |
| |
| /* Setup size is determined by offset 0x0202 + byte value at offset 0x0201, which is the same as |
| * offset of the header field and the target from the jump field (which we split for this reason). */ |
| memcpy(&boot_params->hdr, |
| &image_params->hdr, |
| offsetof(SetupHeader, header) + image_params->hdr.setup_size); |
| |
| boot_params->hdr.type_of_loader = 0xff; |
| |
| /* Spec says: For backwards compatibility, if the setup_sects field contains 0, the real value is 4. */ |
| if (boot_params->hdr.setup_sects == 0) |
| boot_params->hdr.setup_sects = 4; |
| |
| _cleanup_pages_ Pages cmdline_pages = {}; |
| if (cmdline) { |
| size_t len = MIN(strlen16(cmdline), image_params->hdr.cmdline_size); |
| |
| cmdline_pages = xmalloc_pages( |
| can_4g ? AllocateAnyPages : AllocateMaxAddress, |
| EfiLoaderData, |
| EFI_SIZE_TO_PAGES(len + 1), |
| CMDLINE_PTR_MAX); |
| |
| /* Convert cmdline to ASCII. */ |
| char *cmdline8 = PHYSICAL_ADDRESS_TO_POINTER(cmdline_pages.addr); |
| for (size_t i = 0; i < len; i++) |
| cmdline8[i] = cmdline[i] <= 0x7E ? cmdline[i] : ' '; |
| cmdline8[len] = '\0'; |
| |
| boot_params->hdr.cmd_line_ptr = (uint32_t) cmdline_pages.addr; |
| boot_params->ext_cmd_line_ptr = cmdline_pages.addr >> 32; |
| assert(can_4g || cmdline_pages.addr <= CMDLINE_PTR_MAX); |
| } |
| |
| boot_params->hdr.ramdisk_image = (uintptr_t) initrd_buffer; |
| boot_params->ext_ramdisk_image = POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer) >> 32; |
| boot_params->hdr.ramdisk_size = initrd_length; |
| boot_params->ext_ramdisk_size = ((uint64_t) initrd_length) >> 32; |
| |
| log_wait(); |
| linux_efi_handover(parent, (uintptr_t) linux_buffer, boot_params); |
| return EFI_LOAD_ERROR; |
| } |