/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

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

#include "bootimg.h"

#include "uefi_avb_boot.h"
#include "uefi_avb_util.h"

/* See Documentation/x86/boot.txt for this struct and more information
 * about the boot/handover protocol.
 */

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

struct SetupHeader {
  UINT8 boot_sector[0x01f1];
  UINT8 setup_secs;
  UINT16 root_flags;
  UINT32 sys_size;
  UINT16 ram_size;
  UINT16 video_mode;
  UINT16 root_dev;
  UINT16 signature;
  UINT16 jump;
  UINT32 header;
  UINT16 version;
  UINT16 su_switch;
  UINT16 setup_seg;
  UINT16 start_sys;
  UINT16 kernel_ver;
  UINT8 loader_id;
  UINT8 load_flags;
  UINT16 movesize;
  UINT32 code32_start;
  UINT32 ramdisk_start;
  UINT32 ramdisk_len;
  UINT32 bootsect_kludge;
  UINT16 heap_end;
  UINT8 ext_loader_ver;
  UINT8 ext_loader_type;
  UINT32 cmd_line_ptr;
  UINT32 ramdisk_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;
} __attribute__((packed));

#ifdef __x86_64__
typedef VOID (*handover_f)(VOID* image,
                           EFI_SYSTEM_TABLE* table,
                           struct SetupHeader* setup);
static inline VOID linux_efi_handover(EFI_HANDLE image,
                                      struct SetupHeader* setup) {
  handover_f handover;

  asm volatile("cli");
  handover =
      (handover_f)((UINTN)setup->code32_start + 512 + setup->handover_offset);
  handover(image, ST, setup);
}
#else
typedef VOID (*handover_f)(VOID* image,
                           EFI_SYSTEM_TABLE* table,
                           struct SetupHeader* setup)
    __attribute__((regparm(0)));
static inline VOID linux_efi_handover(EFI_HANDLE image,
                                      struct SetupHeader* setup) {
  handover_f handover;

  handover = (handover_f)((UINTN)setup->code32_start + setup->handover_offset);
  handover(image, ST, setup);
}
#endif

static size_t round_up(size_t value, size_t size) {
  size_t ret = value + size - 1;
  ret /= size;
  ret *= size;
  return ret;
}

UEFIAvbBootKernelResult uefi_avb_boot_kernel(EFI_HANDLE efi_image_handle,
                                             AvbSlotVerifyData* slot_data,
                                             const char* cmdline_extra) {
  UEFIAvbBootKernelResult ret;
  const boot_img_hdr* header;
  EFI_STATUS err;
  UINT8* kernel_buf = NULL;
  UINT8* initramfs_buf = NULL;
  UINT8* cmdline_utf8 = NULL;
  AvbPartitionData* boot;
  size_t offset;
  uint64_t total_size;
  size_t initramfs_size;
  size_t cmdline_first_len;
  size_t cmdline_second_len;
  size_t cmdline_extra_len;
  size_t cmdline_utf8_len;
  struct SetupHeader* image_setup;
  struct SetupHeader* setup;
  EFI_PHYSICAL_ADDRESS addr;

  if (slot_data->num_loaded_partitions != 1) {
    avb_error("No boot partition.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT;
    goto out;
  }

  boot = &slot_data->loaded_partitions[0];
  if (avb_strcmp(boot->partition_name, "boot") != 0) {
    avb_errorv(
        "Unexpected partition name '", boot->partition_name, "'.\n", NULL);
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT;
    goto out;
  }

  header = (const boot_img_hdr*)boot->data;

  /* Check boot image header magic field. */
  if (avb_memcmp(BOOT_MAGIC, header->magic, BOOT_MAGIC_SIZE)) {
    avb_error("Wrong boot image header magic.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT;
    goto out;
  }

  /* Sanity check header. */
  total_size = header->kernel_size;
  if (!avb_safe_add_to(&total_size, header->ramdisk_size) ||
      !avb_safe_add_to(&total_size, header->second_size)) {
    avb_error("Overflow while adding sizes of kernel and initramfs.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT;
    goto out;
  }
  if (total_size > boot->data_size) {
    avb_error("Invalid kernel/initramfs sizes.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT;
    goto out;
  }

  /* The kernel has to be in its own specific memory pool. */
  err = uefi_call_wrapper(BS->AllocatePool,
                          NUM_ARGS_ALLOCATE_POOL,
                          EfiLoaderCode,
                          header->kernel_size,
                          &kernel_buf);
  if (EFI_ERROR(err)) {
    avb_error("Could not allocate kernel buffer.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_OOM;
    goto out;
  }
  avb_memcpy(kernel_buf, boot->data + header->page_size, header->kernel_size);

  /* Ditto for the initrd. */
  initramfs_buf = NULL;
  initramfs_size = header->ramdisk_size + header->second_size;
  if (initramfs_size > 0) {
    err = uefi_call_wrapper(BS->AllocatePool,
                            NUM_ARGS_ALLOCATE_POOL,
                            EfiLoaderCode,
                            initramfs_size,
                            &initramfs_buf);
    if (EFI_ERROR(err)) {
      avb_error("Could not allocate initrd buffer.\n");
      ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_OOM;
      goto out;
    }
    /* Concatente the first and second initramfs. */
    offset = header->page_size;
    offset += round_up(header->kernel_size, header->page_size);
    avb_memcpy(initramfs_buf, boot->data + offset, header->ramdisk_size);
    offset += round_up(header->ramdisk_size, header->page_size);
    avb_memcpy(initramfs_buf, boot->data + offset, header->second_size);
  }

  /* Prepare the command-line. */
  cmdline_first_len = avb_strlen((const char*)header->cmdline);
  cmdline_second_len = avb_strlen(slot_data->cmdline);
  cmdline_extra_len = cmdline_extra != NULL ? avb_strlen(cmdline_extra) : 0;
  if (cmdline_extra_len > 0) {
    cmdline_extra_len += 1;
  }
  cmdline_utf8_len =
      cmdline_first_len + 1 + cmdline_second_len + 1 + cmdline_extra_len;
  err = uefi_call_wrapper(BS->AllocatePool,
                          NUM_ARGS_ALLOCATE_POOL,
                          EfiLoaderCode,
                          cmdline_utf8_len,
                          &cmdline_utf8);
  if (EFI_ERROR(err)) {
    avb_error("Could not allocate kernel cmdline.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_OOM;
    goto out;
  }
  offset = 0;
  avb_memcpy(cmdline_utf8, header->cmdline, cmdline_first_len);
  offset += cmdline_first_len;
  cmdline_utf8[offset] = ' ';
  offset += 1;
  avb_memcpy(cmdline_utf8 + offset, slot_data->cmdline, cmdline_second_len);
  offset += cmdline_second_len;
  if (cmdline_extra_len > 0) {
    cmdline_utf8[offset] = ' ';
    avb_memcpy(cmdline_utf8 + offset + 1, cmdline_extra, cmdline_extra_len - 1);
    offset += cmdline_extra_len;
  }
  cmdline_utf8[offset] = '\0';
  offset += 1;
  avb_assert(offset == cmdline_utf8_len);

  /* Now set up the EFI handover. */
  image_setup = (struct SetupHeader*)kernel_buf;
  if (image_setup->signature != 0xAA55 || image_setup->header != SETUP_MAGIC) {
    avb_error("Wrong kernel header magic.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_KERNEL_INVALID_FORMAT;
    goto out;
  }

  if (image_setup->version < 0x20b) {
    avb_error("Wrong version.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_KERNEL_INVALID_FORMAT;
    goto out;
  }

  if (!image_setup->relocatable_kernel) {
    avb_error("Kernel is not relocatable.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_KERNEL_INVALID_FORMAT;
    goto out;
  }

  addr = 0x3fffffff;
  err = uefi_call_wrapper(BS->AllocatePages,
                          4,
                          AllocateMaxAddress,
                          EfiLoaderData,
                          EFI_SIZE_TO_PAGES(0x4000),
                          &addr);
  if (EFI_ERROR(err)) {
    avb_error("Could not allocate setup buffer.\n");
    ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_OOM;
    goto out;
  }
  setup = (struct SetupHeader*)(UINTN)addr;
  avb_memset(setup, '\0', 0x4000);
  avb_memcpy(setup, image_setup, sizeof(struct SetupHeader));
  setup->loader_id = 0xff;
  setup->code32_start =
      ((uintptr_t)kernel_buf) + (image_setup->setup_secs + 1) * 512;
  setup->cmd_line_ptr = (uintptr_t)cmdline_utf8;

  setup->ramdisk_start = (uintptr_t)initramfs_buf;
  setup->ramdisk_len = (uintptr_t)initramfs_size;

  /* Jump to the kernel. */
  linux_efi_handover(efi_image_handle, setup);

  ret = UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_START_KERNEL;
out:
  return ret;
}

const char* uefi_avb_boot_kernel_result_to_string(
    UEFIAvbBootKernelResult result) {
  const char* ret = NULL;

  switch (result) {
    case UEFI_AVB_BOOT_KERNEL_RESULT_OK:
      ret = "OK";
      break;
    case UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_OOM:
      ret = "ERROR_OEM";
      break;
    case UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_IO:
      ret = "ERROR_IO";
      break;
    case UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_PARTITION_INVALID_FORMAT:
      ret = "ERROR_PARTITION_INVALID_FORMAT";
      break;
    case UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_KERNEL_INVALID_FORMAT:
      ret = "ERROR_KERNEL_INVALID_FORMAT";
      break;
    case UEFI_AVB_BOOT_KERNEL_RESULT_ERROR_START_KERNEL:
      ret = "ERROR_START_KERNEL";
      break;
      /* Do not add a 'default:' case here because of -Wswitch. */
  }

  if (ret == NULL) {
    avb_error("Unknown UEFIAvbBootKernelResult value.\n");
    ret = "(unknown)";
  }

  return ret;
}
