| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| /* |
| * Generic Linux boot protocol using the EFI/PE entry point of the kernel. Passes |
| * initrd with the LINUX_INITRD_MEDIA_GUID DevicePath and cmdline with |
| * EFI LoadedImageProtocol. |
| * |
| * This method works for Linux 5.8 and newer on ARM/Aarch64, x86/x68_64 and RISC-V. |
| */ |
| |
| #include <efi.h> |
| #include <efilib.h> |
| |
| #include "initrd.h" |
| #include "linux.h" |
| #include "pe.h" |
| #include "util.h" |
| |
| static EFI_LOADED_IMAGE * loaded_image_free(EFI_LOADED_IMAGE *img) { |
| if (!img) |
| return NULL; |
| mfree(img->LoadOptions); |
| return mfree(img); |
| } |
| |
| static EFI_STATUS loaded_image_register( |
| const CHAR8 *cmdline, UINTN cmdline_len, |
| const void *linux_buffer, UINTN linux_length, |
| EFI_HANDLE *ret_image) { |
| |
| EFI_LOADED_IMAGE *loaded_image = NULL; |
| EFI_STATUS err; |
| |
| assert(cmdline || cmdline_len > 0); |
| assert(linux_buffer && linux_length > 0); |
| assert(ret_image); |
| |
| /* create and install new LoadedImage Protocol */ |
| loaded_image = xnew(EFI_LOADED_IMAGE, 1); |
| *loaded_image = (EFI_LOADED_IMAGE) { |
| .ImageBase = (void *) linux_buffer, |
| .ImageSize = linux_length |
| }; |
| |
| /* if a cmdline is set convert it to UCS2 */ |
| if (cmdline) { |
| loaded_image->LoadOptions = xstra_to_str(cmdline); |
| loaded_image->LoadOptionsSize = StrSize(loaded_image->LoadOptions); |
| } |
| |
| /* install a new LoadedImage protocol. ret_handle is a new image handle */ |
| err = BS->InstallMultipleProtocolInterfaces( |
| ret_image, |
| &LoadedImageProtocol, loaded_image, |
| NULL); |
| if (EFI_ERROR(err)) |
| loaded_image = loaded_image_free(loaded_image); |
| |
| return err; |
| } |
| |
| static EFI_STATUS loaded_image_unregister(EFI_HANDLE loaded_image_handle) { |
| EFI_LOADED_IMAGE_PROTOCOL *loaded_image; |
| EFI_STATUS err; |
| |
| if (!loaded_image_handle) |
| return EFI_SUCCESS; |
| |
| /* get the LoadedImage protocol that we allocated earlier */ |
| err = BS->OpenProtocol( |
| loaded_image_handle, &LoadedImageProtocol, (void **) &loaded_image, |
| NULL, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); |
| if (EFI_ERROR(err)) |
| return err; |
| |
| /* close the handle */ |
| (void) BS->CloseProtocol(loaded_image_handle, &LoadedImageProtocol, NULL, NULL); |
| err = BS->UninstallMultipleProtocolInterfaces( |
| loaded_image_handle, |
| &LoadedImageProtocol, loaded_image, |
| NULL); |
| if (EFI_ERROR(err)) |
| return err; |
| loaded_image_handle = NULL; |
| loaded_image = loaded_image_free(loaded_image); |
| |
| return EFI_SUCCESS; |
| } |
| |
| static inline void cleanup_initrd(EFI_HANDLE *initrd_handle) { |
| (void) initrd_unregister(*initrd_handle); |
| *initrd_handle = NULL; |
| } |
| |
| static inline void cleanup_loaded_image(EFI_HANDLE *loaded_image_handle) { |
| (void) loaded_image_unregister(*loaded_image_handle); |
| *loaded_image_handle = NULL; |
| } |
| |
| /* struct to call cleanup_pages */ |
| struct pages { |
| EFI_PHYSICAL_ADDRESS addr; |
| UINTN num; |
| }; |
| |
| static inline void cleanup_pages(struct pages *p) { |
| if (p->addr == 0) |
| return; |
| (void) BS->FreePages(p->addr, p->num); |
| } |
| |
| EFI_STATUS linux_exec( |
| EFI_HANDLE image, |
| const CHAR8 *cmdline, UINTN cmdline_len, |
| const void *linux_buffer, UINTN linux_length, |
| const void *initrd_buffer, UINTN initrd_length) { |
| |
| _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; |
| _cleanup_(cleanup_loaded_image) EFI_HANDLE loaded_image_handle = NULL; |
| UINT32 kernel_alignment, kernel_size_of_image, kernel_entry_address; |
| EFI_IMAGE_ENTRY_POINT kernel_entry; |
| _cleanup_(cleanup_pages) struct pages kernel = {}; |
| void *new_buffer; |
| EFI_STATUS err; |
| |
| assert(image); |
| assert(cmdline || cmdline_len == 0); |
| assert(linux_buffer && linux_length > 0); |
| assert(initrd_buffer || initrd_length == 0); |
| |
| /* get the necessary fields from the PE header */ |
| err = pe_alignment_info(linux_buffer, &kernel_entry_address, &kernel_size_of_image, &kernel_alignment); |
| if (EFI_ERROR(err)) |
| return err; |
| /* sanity check */ |
| assert(kernel_size_of_image >= linux_length); |
| |
| /* Linux kernel complains if it's not loaded at a properly aligned memory address. The correct alignment |
| is provided by Linux as the SegmentAlignment in the PeOptionalHeader. Additionally the kernel needs to |
| be in a memory segment that's SizeOfImage (again from PeOptionalHeader) large, so that the Kernel has |
| space for its BSS section. SizeOfImage is always larger than linux_length, which is only the size of |
| Code, (static) Data and Headers. |
| |
| Interrestingly only ARM/Aarch64 and RISC-V kernel stubs check these assertions and can even boot (with warnings) |
| if they are not met. x86 and x86_64 kernel stubs don't do checks and fail if the BSS section is too small. |
| */ |
| /* allocate SizeOfImage + SectionAlignment because the new_buffer can move up to Alignment-1 bytes */ |
| kernel.num = EFI_SIZE_TO_PAGES(ALIGN_TO(kernel_size_of_image, kernel_alignment) + kernel_alignment); |
| err = BS->AllocatePages(AllocateAnyPages, EfiLoaderData, kernel.num, &kernel.addr); |
| if (EFI_ERROR(err)) |
| return EFI_OUT_OF_RESOURCES; |
| new_buffer = PHYSICAL_ADDRESS_TO_POINTER(ALIGN_TO(kernel.addr, kernel_alignment)); |
| CopyMem(new_buffer, linux_buffer, linux_length); |
| /* zero out rest of memory (probably not needed, but BSS section should be 0) */ |
| SetMem((UINT8 *)new_buffer + linux_length, kernel_size_of_image - linux_length, 0); |
| |
| /* get the entry point inside the relocated kernel */ |
| kernel_entry = (EFI_IMAGE_ENTRY_POINT) ((const UINT8 *)new_buffer + kernel_entry_address); |
| |
| /* register a LoadedImage Protocol in order to pass on the commandline */ |
| err = loaded_image_register(cmdline, cmdline_len, new_buffer, linux_length, &loaded_image_handle); |
| if (EFI_ERROR(err)) |
| return err; |
| |
| /* register a LINUX_INITRD_MEDIA DevicePath to serve the initrd */ |
| err = initrd_register(initrd_buffer, initrd_length, &initrd_handle); |
| if (EFI_ERROR(err)) |
| return err; |
| |
| /* call the kernel */ |
| return kernel_entry(loaded_image_handle, ST); |
| } |