/** @file

  Copyright (c) 2021, Intel Corporation. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include "CcExitTd.h"
#include <Library/CcExitLib.h>
#include <Library/BaseMemoryLib.h>
#include <IndustryStandard/Tdx.h>
#include <IndustryStandard/InstructionParsing.h>

typedef union {
  struct {
    UINT32    Eax;
    UINT32    Edx;
  } Regs;
  UINT64    Val;
} MSR_DATA;

typedef union {
  UINT8    Val;
  struct {
    UINT8    B : 1;
    UINT8    X : 1;
    UINT8    R : 1;
    UINT8    W : 1;
  } Bits;
} REX;

typedef union {
  UINT8    Val;
  struct {
    UINT8    Rm  : 3;
    UINT8    Reg : 3;
    UINT8    Mod : 2;
  } Bits;
} MODRM;

typedef struct {
  UINT64    Regs[4];
} CPUID_DATA;

/**
  Handle an CPUID event.

  Use the TDVMCALL instruction to handle cpuid #ve

  @param[in, out] Regs             x64 processor context
  @param[in]      Veinfo           VE Info

  @retval 0                        Event handled successfully
  @return                          New exception value to propagate
**/
STATIC
UINT64
EFIAPI
CpuIdExit (
  IN EFI_SYSTEM_CONTEXT_X64     *Regs,
  IN TDCALL_VEINFO_RETURN_DATA  *Veinfo
  )
{
  CPUID_DATA  CpuIdData;
  UINT64      Status;

  Status = TdVmCallCpuid (Regs->Rax, Regs->Rcx, &CpuIdData);

  if (Status == 0) {
    Regs->Rax = CpuIdData.Regs[0];
    Regs->Rbx = CpuIdData.Regs[1];
    Regs->Rcx = CpuIdData.Regs[2];
    Regs->Rdx = CpuIdData.Regs[3];
  }

  return Status;
}

/**
  Handle an IO event.

  Use the TDVMCALL instruction to handle either an IO read or an IO write.

  @param[in, out] Regs             x64 processor context
  @param[in]      Veinfo           VE Info

  @retval 0                        Event handled successfully
  @return                          New exception value to propagate
**/
STATIC
UINT64
EFIAPI
IoExit (
  IN OUT EFI_SYSTEM_CONTEXT_X64  *Regs,
  IN TDCALL_VEINFO_RETURN_DATA   *Veinfo
  )
{
  BOOLEAN  Write;
  UINTN    Size;
  UINTN    Port;
  UINT64   Val;
  UINT64   RepCnt;
  UINT64   Status;

  Val   = 0;
  Write = Veinfo->ExitQualification.Io.Direction ? FALSE : TRUE;
  Size  = Veinfo->ExitQualification.Io.Size + 1;
  Port  = Veinfo->ExitQualification.Io.Port;

  if (Veinfo->ExitQualification.Io.String) {
    //
    // If REP is set, get rep-cnt from Rcx
    //
    RepCnt = Veinfo->ExitQualification.Io.Rep ? Regs->Rcx : 1;

    while (RepCnt) {
      Val = 0;
      if (Write == TRUE) {
        CopyMem (&Val, (VOID *)Regs->Rsi, Size);
        Regs->Rsi += Size;
      }

      Status = TdVmCall (EXIT_REASON_IO_INSTRUCTION, Size, Write, Port, Val, (Write ? NULL : &Val));
      if (Status != 0) {
        break;
      }

      if (Write == FALSE) {
        CopyMem ((VOID *)Regs->Rdi, &Val, Size);
        Regs->Rdi += Size;
      }

      if (Veinfo->ExitQualification.Io.Rep) {
        Regs->Rcx -= 1;
      }

      RepCnt -= 1;
    }
  } else {
    if (Write == TRUE) {
      CopyMem (&Val, (VOID *)&Regs->Rax, Size);
    }

    Status = TdVmCall (EXIT_REASON_IO_INSTRUCTION, Size, Write, Port, Val, (Write ? NULL : &Val));
    if ((Status == 0) && (Write == FALSE)) {
      CopyMem ((VOID *)&Regs->Rax, &Val, Size);
    }
  }

  return Status;
}

/**
  Handle an READ MSR event.

  Use the TDVMCALL instruction to handle msr read

  @param[in, out] Regs             x64 processor context
  @param[in]      Veinfo           VE Info

  @retval 0                        Event handled successfully
  @return                          New exception value to propagate
**/
STATIC
UINT64
ReadMsrExit (
  IN OUT EFI_SYSTEM_CONTEXT_X64  *Regs,
  IN TDCALL_VEINFO_RETURN_DATA   *Veinfo
  )
{
  MSR_DATA  Data;
  UINT64    Status;

  Status = TdVmCall (EXIT_REASON_MSR_READ, Regs->Rcx, 0, 0, 0, &Data);
  if (Status == 0) {
    Regs->Rax = Data.Regs.Eax;
    Regs->Rdx = Data.Regs.Edx;
  }

  return Status;
}

/**
  Handle an WRITE MSR event.

  Use the TDVMCALL instruction to handle msr write

  @param[in, out] Regs             x64 processor context
  @param[in]      Veinfo           VE Info

  @retval 0                        Event handled successfully
  @return                          New exception value to propagate
**/
STATIC
UINT64
WriteMsrExit (
  IN OUT EFI_SYSTEM_CONTEXT_X64  *Regs,
  IN TDCALL_VEINFO_RETURN_DATA   *Veinfo
  )
{
  UINT64    Status;
  MSR_DATA  Data;

  Data.Regs.Eax = (UINT32)Regs->Rax;
  Data.Regs.Edx = (UINT32)Regs->Rdx;

  Status =  TdVmCall (EXIT_REASON_MSR_WRITE, Regs->Rcx, Data.Val, 0, 0, NULL);

  return Status;
}

STATIC
VOID
EFIAPI
TdxDecodeInstruction (
  IN UINT8  *Rip
  )
{
  UINTN  i;

  DEBUG ((DEBUG_INFO, "TDX: #TD[EPT] instruction (%p):", Rip));
  for (i = 0; i < 15; i++) {
    DEBUG ((DEBUG_INFO, "%02x:", Rip[i]));
  }

  DEBUG ((DEBUG_INFO, "\n"));
}

#define TDX_DECODER_BUG_ON(x)               \
  if ((x)) {                                \
    TdxDecodeInstruction(Rip);              \
    TdVmCall(TDVMCALL_HALT, 0, 0, 0, 0, 0); \
  }

STATIC
UINT64 *
EFIAPI
GetRegFromContext (
  IN EFI_SYSTEM_CONTEXT_X64  *Regs,
  IN UINTN                   RegIndex
  )
{
  switch (RegIndex) {
    case 0: return &Regs->Rax;
      break;
    case 1: return &Regs->Rcx;
      break;
    case 2: return &Regs->Rdx;
      break;
    case 3: return &Regs->Rbx;
      break;
    case 4: return &Regs->Rsp;
      break;
    case 5: return &Regs->Rbp;
      break;
    case 6: return &Regs->Rsi;
      break;
    case 7: return &Regs->Rdi;
      break;
    case 8: return &Regs->R8;
      break;
    case 9: return &Regs->R9;
      break;
    case 10: return &Regs->R10;
      break;
    case 11: return &Regs->R11;
      break;
    case 12: return &Regs->R12;
      break;
    case 13: return &Regs->R13;
      break;
    case 14: return &Regs->R14;
      break;
    case 15: return &Regs->R15;
      break;
  }

  return NULL;
}

/**
  Handle an MMIO event.

  Use the TDVMCALL instruction to handle either an mmio read or an mmio write.

  @param[in, out] Regs             x64 processor context
  @param[in]      Veinfo           VE Info

  @retval 0                        Event handled successfully
  @return                          New exception value to propagate
**/
STATIC
INTN
EFIAPI
MmioExit (
  IN OUT EFI_SYSTEM_CONTEXT_X64  *Regs,
  IN TDCALL_VEINFO_RETURN_DATA   *Veinfo
  )
{
  UINT64          Status;
  UINT32          MmioSize;
  UINT32          RegSize;
  UINT8           OpCode;
  BOOLEAN         SeenRex;
  UINT64          *Reg;
  UINT8           *Rip;
  UINT64          Val;
  UINT32          OpSize;
  MODRM           ModRm;
  REX             Rex;
  TD_RETURN_DATA  TdReturnData;
  UINT8           Gpaw;
  UINT64          TdSharedPageMask;

  Rip     = (UINT8 *)Regs->Rip;
  Val     = 0;
  Rex.Val = 0;
  SeenRex = FALSE;

  Status = TdCall (TDCALL_TDINFO, 0, 0, 0, &TdReturnData);
  if (Status == TDX_EXIT_REASON_SUCCESS) {
    Gpaw             = (UINT8)(TdReturnData.TdInfo.Gpaw & 0x3f);
    TdSharedPageMask = 1ULL << (Gpaw - 1);
  } else {
    DEBUG ((DEBUG_ERROR, "TDCALL failed with status=%llx\n", Status));
    return Status;
  }

  if ((Veinfo->GuestPA & TdSharedPageMask) == 0) {
    DEBUG ((DEBUG_ERROR, "EPT-violation #VE on private memory is not allowed!"));
    TdVmCall (TDVMCALL_HALT, 0, 0, 0, 0, 0);
    CpuDeadLoop ();
  }

  //
  // Default to 32bit transfer
  //
  OpSize = 4;

  do {
    OpCode = *Rip++;
    if (OpCode == 0x66) {
      OpSize = 2;
    } else if ((OpCode == 0x64) || (OpCode == 0x65) || (OpCode == 0x67)) {
      continue;
    } else if ((OpCode >= 0x40) && (OpCode <= 0x4f)) {
      SeenRex = TRUE;
      Rex.Val = OpCode;
    } else {
      break;
    }
  } while (TRUE);

  //
  // We need to have at least 2 more bytes for this instruction
  //
  TDX_DECODER_BUG_ON (((UINT64)Rip - Regs->Rip) > 13);

  OpCode = *Rip++;
  //
  // Two-byte opecode, get next byte
  //
  if (OpCode == 0x0F) {
    OpCode = *Rip++;
  }

  switch (OpCode) {
    case 0x88:
    case 0x8A:
    case 0xB6:
      MmioSize = 1;
      break;
    case 0xB7:
      MmioSize = 2;
      break;
    default:
      MmioSize = Rex.Bits.W ? 8 : OpSize;
      break;
  }

  /* Punt on AH/BH/CH/DH unless it shows up. */
  ModRm.Val = *Rip++;
  TDX_DECODER_BUG_ON (MmioSize == 1 && ModRm.Bits.Reg > 4 && !SeenRex && OpCode != 0xB6);
  Reg = GetRegFromContext (Regs, ModRm.Bits.Reg | ((int)Rex.Bits.R << 3));
  TDX_DECODER_BUG_ON (!Reg);

  if (ModRm.Bits.Rm == 4) {
    ++Rip; /* SIB byte */
  }

  if ((ModRm.Bits.Mod == 2) || ((ModRm.Bits.Mod == 0) && (ModRm.Bits.Rm == 5))) {
    Rip += 4; /* DISP32 */
  } else if (ModRm.Bits.Mod == 1) {
    ++Rip;  /* DISP8 */
  }

  switch (OpCode) {
    case 0x88:
    case 0x89:
      CopyMem ((void *)&Val, Reg, MmioSize);
      Status = TdVmCall (TDVMCALL_MMIO, MmioSize, 1, Veinfo->GuestPA, Val, 0);
      break;
    case 0xC7:
      CopyMem ((void *)&Val, Rip, OpSize);
      Status = TdVmCall (TDVMCALL_MMIO, MmioSize, 1, Veinfo->GuestPA, Val, 0);
      Rip   += OpSize;
    default:
      //
      // 32-bit write registers are zero extended to the full register
      // Hence 'MOVZX r[32/64], r/m16' is
      // hardcoded to reg size 8, and the straight MOV case has a reg
      // size of 8 in the 32-bit read case.
      //
      switch (OpCode) {
        case 0xB6:
          RegSize = Rex.Bits.W ? 8 : OpSize;
          break;
        case 0xB7:
          RegSize =  8;
          break;
        default:
          RegSize = MmioSize == 4 ? 8 : MmioSize;
          break;
      }

      Status = TdVmCall (TDVMCALL_MMIO, MmioSize, 0, Veinfo->GuestPA, 0, &Val);
      if (Status == 0) {
        ZeroMem (Reg, RegSize);
        CopyMem (Reg, (void *)&Val, MmioSize);
      }
  }

  if (Status == 0) {
    TDX_DECODER_BUG_ON (((UINT64)Rip - Regs->Rip) > 15);

    //
    // We change instruction length to reflect true size so handler can
    // bump rip
    //
    Veinfo->ExitInstructionLength =  (UINT32)((UINT64)Rip - Regs->Rip);
  }

  return Status;
}

/**
  Handle a #VE exception.

  Performs the necessary processing to handle a #VE exception.

  @param[in, out]  ExceptionType  Pointer to an EFI_EXCEPTION_TYPE to be set
                                  as value to use on error.
  @param[in, out]  SystemContext  Pointer to EFI_SYSTEM_CONTEXT

  @retval  EFI_SUCCESS            Exception handled
  @retval  EFI_UNSUPPORTED        #VE not supported, (new) exception value to
                                  propagate provided
  @retval  EFI_PROTOCOL_ERROR     #VE handling failed, (new) exception value to
                                  propagate provided

**/
EFI_STATUS
EFIAPI
CcExitHandleVe (
  IN OUT EFI_EXCEPTION_TYPE  *ExceptionType,
  IN OUT EFI_SYSTEM_CONTEXT  SystemContext
  )
{
  UINT64                  Status;
  TD_RETURN_DATA          ReturnData;
  EFI_SYSTEM_CONTEXT_X64  *Regs;

  Regs   = SystemContext.SystemContextX64;
  Status = TdCall (TDCALL_TDGETVEINFO, 0, 0, 0, &ReturnData);
  ASSERT (Status == 0);
  if (Status != 0) {
    DEBUG ((DEBUG_ERROR, "#VE happened. TDGETVEINFO failed with Status = 0x%llx\n", Status));
    TdVmCall (TDVMCALL_HALT, 0, 0, 0, 0, 0);
  }

  switch (ReturnData.VeInfo.ExitReason) {
    case EXIT_REASON_CPUID:
      Status = CpuIdExit (Regs, &ReturnData.VeInfo);
      DEBUG ((
        DEBUG_VERBOSE,
        "CPUID #VE happened, ExitReasion is %d, ExitQualification = 0x%x.\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val
        ));
      break;

    case EXIT_REASON_HLT:
      Status = TdVmCall (EXIT_REASON_HLT, 0, 0, 0, 0, 0);
      break;

    case EXIT_REASON_IO_INSTRUCTION:
      Status = IoExit (Regs, &ReturnData.VeInfo);
      DEBUG ((
        DEBUG_VERBOSE,
        "IO_Instruction #VE happened, ExitReasion is %d, ExitQualification = 0x%x.\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val
        ));
      break;

    case EXIT_REASON_MSR_READ:
      Status = ReadMsrExit (Regs, &ReturnData.VeInfo);
      DEBUG ((
        DEBUG_VERBOSE,
        "RDMSR #VE happened, ExitReasion is %d, ExitQualification = 0x%x. Regs->Rcx=0x%llx, Status = 0x%llx\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val,
        Regs->Rcx,
        Status
        ));
      break;

    case EXIT_REASON_MSR_WRITE:
      Status = WriteMsrExit (Regs, &ReturnData.VeInfo);
      DEBUG ((
        DEBUG_VERBOSE,
        "WRMSR #VE happened, ExitReasion is %d, ExitQualification = 0x%x. Regs->Rcx=0x%llx, Status = 0x%llx\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val,
        Regs->Rcx,
        Status
        ));
      break;

    case EXIT_REASON_EPT_VIOLATION:
      Status = MmioExit (Regs, &ReturnData.VeInfo);
      DEBUG ((
        DEBUG_VERBOSE,
        "MMIO #VE happened, ExitReasion is %d, ExitQualification = 0x%x.\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val
        ));
      break;

    case EXIT_REASON_VMCALL:
    case EXIT_REASON_MWAIT_INSTRUCTION:
    case EXIT_REASON_MONITOR_INSTRUCTION:
    case EXIT_REASON_WBINVD:
    case EXIT_REASON_RDPMC:
    case EXIT_REASON_INVD:
      /* Handle as nops. */
      break;

    default:
      DEBUG ((
        DEBUG_ERROR,
        "Unsupported #VE happened, ExitReason is %d, ExitQualification = 0x%x.\n",
        ReturnData.VeInfo.ExitReason,
        ReturnData.VeInfo.ExitQualification.Val
        ));

      ASSERT (FALSE);
      CpuDeadLoop ();
  }

  if (Status) {
    DEBUG ((
      DEBUG_ERROR,
      "#VE Error (0x%llx) returned from host, ExitReason is %d, ExitQualification = 0x%x.\n",
      Status,
      ReturnData.VeInfo.ExitReason,
      ReturnData.VeInfo.ExitQualification.Val
      ));

    TdVmCall (TDVMCALL_HALT, 0, 0, 0, 0, 0);
  }

  SystemContext.SystemContextX64->Rip += ReturnData.VeInfo.ExitInstructionLength;
  return EFI_SUCCESS;
}
