/* 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://www.kernel.org/doc/html/latest/x86/boot.html
 */

#include <efi.h>
#include <efilib.h>

#include "initrd.h"
#include "linux.h"
#include "macro-fundamental.h"
#include "util.h"

#define SETUP_MAGIC             0x53726448      /* "HdrS" */

struct setup_header {
        UINT8  setup_sects;
        UINT16 root_flags;
        UINT32 syssize;
        UINT16 ram_size;
        UINT16 vid_mode;
        UINT16 root_dev;
        UINT16 boot_flag;
        UINT16 jump;
        UINT32 header;
        UINT16 version;
        UINT32 realmode_swtch;
        UINT16 start_sys_seg;
        UINT16 kernel_version;
        UINT8  type_of_loader;
        UINT8  loadflags;
        UINT16 setup_move_size;
        UINT32 code32_start;
        UINT32 ramdisk_image;
        UINT32 ramdisk_size;
        UINT32 bootsect_kludge;
        UINT16 heap_end_ptr;
        UINT8  ext_loader_ver;
        UINT8  ext_loader_type;
        UINT32 cmd_line_ptr;
        UINT32 initrd_addr_max;
        UINT32 kernel_alignment;
        UINT8  relocatable_kernel;
        UINT8  min_alignment;
        UINT16 xloadflags;
        UINT32 cmdline_size;
        UINT32 hardware_subarch;
        UINT64 hardware_subarch_data;
        UINT32 payload_offset;
        UINT32 payload_length;
        UINT64 setup_data;
        UINT64 pref_address;
        UINT32 init_size;
        UINT32 handover_offset;
} _packed_;

/* adapted from linux' bootparam.h */
struct boot_params {
        UINT8  screen_info[64];         // was: struct screen_info
        UINT8  apm_bios_info[20];       // was: struct apm_bios_info
        UINT8  _pad2[4];
        UINT64 tboot_addr;
        UINT8  ist_info[16];            // was: struct ist_info
        UINT8  _pad3[16];
        UINT8  hd0_info[16];
        UINT8  hd1_info[16];
        UINT8  sys_desc_table[16];      // was: struct sys_desc_table
        UINT8  olpc_ofw_header[16];     // was: struct olpc_ofw_header
        UINT32 ext_ramdisk_image;
        UINT32 ext_ramdisk_size;
        UINT32 ext_cmd_line_ptr;
        UINT8  _pad4[116];
        UINT8  edid_info[128];          // was: struct edid_info
        UINT8  efi_info[32];            // was: struct efi_info
        UINT32 alt_mem_k;
        UINT32 scratch;
        UINT8  e820_entries;
        UINT8  eddbuf_entries;
        UINT8  edd_mbr_sig_buf_entries;
        UINT8  kbd_status;
        UINT8  secure_boot;
        UINT8  _pad5[2];
        UINT8  sentinel;
        UINT8  _pad6[1];
        struct setup_header hdr;
        UINT8  _pad7[0x290-0x1f1-sizeof(struct setup_header)];
        UINT32 edd_mbr_sig_buffer[16];  // was: edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]
        UINT8  e820_table[20*128];      // was: struct boot_e820_entry e820_table[E820_MAX_ENTRIES_ZEROPAGE]
        UINT8  _pad8[48];
        UINT8  eddbuf[6*82];            // was: struct edd_info eddbuf[EDDMAXNR]
        UINT8  _pad9[276];
} _packed_;

#ifdef __i386__
#define __regparm0__ __attribute__((regparm(0)))
#else
#define __regparm0__
#endif

typedef void(*handover_f)(void *image, EFI_SYSTEM_TABLE *table, struct boot_params *params) __regparm0__;

static void linux_efi_handover(EFI_HANDLE image, struct boot_params *params) {
        handover_f handover;
        UINTN start = (UINTN)params->hdr.code32_start;

        assert(params);

#ifdef __x86_64__
        asm volatile ("cli");
        start += 512;
#endif
        handover = (handover_f)(start + params->hdr.handover_offset);
        handover(image, ST, params);
}

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) {

        const struct boot_params *image_params;
        struct boot_params *boot_params;
        EFI_HANDLE initrd_handle = NULL;
        EFI_PHYSICAL_ADDRESS addr;
        UINT8 setup_sectors;
        EFI_STATUS err;

        assert(image);
        assert(cmdline || cmdline_len == 0);
        assert(linux_buffer);
        assert(initrd_buffer || initrd_length == 0);

        if (linux_length < sizeof(struct boot_params))
                return EFI_LOAD_ERROR;

        image_params = (const struct boot_params *) linux_buffer;

        if (image_params->hdr.boot_flag != 0xAA55 ||
            image_params->hdr.header != SETUP_MAGIC ||
            image_params->hdr.version < 0x20b ||
            !image_params->hdr.relocatable_kernel)
                return EFI_LOAD_ERROR;

        addr = UINT32_MAX; /* Below the 32bit boundary */
        err = BS->AllocatePages(
                        AllocateMaxAddress,
                        EfiLoaderData,
                        EFI_SIZE_TO_PAGES(0x4000),
                        &addr);
        if (EFI_ERROR(err))
                return err;

        boot_params = (struct boot_params *) PHYSICAL_ADDRESS_TO_POINTER(addr);
        ZeroMem(boot_params, 0x4000);
        boot_params->hdr = image_params->hdr;
        boot_params->hdr.type_of_loader = 0xff;
        setup_sectors = image_params->hdr.setup_sects > 0 ? image_params->hdr.setup_sects : 4;
        boot_params->hdr.code32_start = (UINT32) POINTER_TO_PHYSICAL_ADDRESS(linux_buffer) + (setup_sectors + 1) * 512;

        if (cmdline) {
                addr = 0xA0000;

                err = BS->AllocatePages(
                                AllocateMaxAddress,
                                EfiLoaderData,
                                EFI_SIZE_TO_PAGES(cmdline_len + 1),
                                &addr);
                if (EFI_ERROR(err))
                        return err;

                CopyMem(PHYSICAL_ADDRESS_TO_POINTER(addr), cmdline, cmdline_len);
                ((CHAR8 *) PHYSICAL_ADDRESS_TO_POINTER(addr))[cmdline_len] = 0;
                boot_params->hdr.cmd_line_ptr = (UINT32) addr;
        }

        /* Providing the initrd via LINUX_INITRD_MEDIA_GUID is only supported by Linux 5.8+ (5.7+ on ARM64).
           Until supported kernels become more established, we continue to set ramdisk in the handover struct.
           This value is overridden by kernels that support LINUX_INITRD_MEDIA_GUID.
           If you need to know which protocol was used by the kernel, pass "efi=debug" to the kernel,
           this will print a line when InitrdMediaGuid was successfully used to load the initrd.
         */
        boot_params->hdr.ramdisk_image = (UINT32) POINTER_TO_PHYSICAL_ADDRESS(initrd_buffer);
        boot_params->hdr.ramdisk_size = (UINT32) initrd_length;

        /* register LINUX_INITRD_MEDIA_GUID */
        err = initrd_register(initrd_buffer, initrd_length, &initrd_handle);
        if (EFI_ERROR(err))
                return err;
        linux_efi_handover(image, boot_params);
        (void) initrd_unregister(initrd_handle);
        initrd_handle = NULL;
        return EFI_LOAD_ERROR;
}
