/**@file
  Initialize Secure Encrypted Virtualization (SEV) support

  Copyright (c) 2017 - 2024, Advanced Micro Devices. All rights reserved.<BR>

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

**/
//
// The package level header files this module uses
//
#include <Guid/GhcbApicIds.h>
#include <IndustryStandard/Q35MchIch9.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/HobLib.h>
#include <Library/MemEncryptSevLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include <Pi/PiHob.h>
#include <PiPei.h>
#include <Register/Amd/Msr.h>
#include <Register/Intel/SmramSaveStateMap.h>
#include <Library/CcExitLib.h>
#include <ConfidentialComputingGuestAttr.h>

#include "Platform.h"

STATIC
UINT64
GetHypervisorFeature (
  VOID
  );

/**
  Retrieve APIC IDs from the hypervisor.

**/
STATIC
VOID
AmdSevSnpGetApicIds (
  VOID
  )
{
  MSR_SEV_ES_GHCB_REGISTER  Msr;
  GHCB                      *Ghcb;
  BOOLEAN                   InterruptState;
  UINT64                    VmgExitStatus;
  UINT64                    PageCount;
  BOOLEAN                   PageCountValid;
  VOID                      *ApicIds;
  RETURN_STATUS             Status;
  UINT64                    GuidData;

  Msr.GhcbPhysicalAddress = AsmReadMsr64 (MSR_SEV_ES_GHCB);
  Ghcb                    = Msr.Ghcb;

  PageCount      = 0;
  PageCountValid = FALSE;

  CcExitVmgInit (Ghcb, &InterruptState);
  Ghcb->SaveArea.Rax = PageCount;
  CcExitVmgSetOffsetValid (Ghcb, GhcbRax);
  VmgExitStatus = CcExitVmgExit (Ghcb, SVM_EXIT_GET_APIC_IDS, 0, 0);
  if (CcExitVmgIsOffsetValid (Ghcb, GhcbRax)) {
    PageCount      = Ghcb->SaveArea.Rax;
    PageCountValid = TRUE;
  }

  CcExitVmgDone (Ghcb, InterruptState);

  ASSERT (VmgExitStatus == 0);
  ASSERT (PageCountValid);
  if ((VmgExitStatus != 0) || !PageCountValid) {
    return;
  }

  //
  // Allocate the memory for the APIC IDs
  //
  ApicIds = AllocateReservedPages ((UINTN)PageCount);
  ASSERT (ApicIds != NULL);

  Status = MemEncryptSevClearPageEncMask (
             0,
             (UINTN)ApicIds,
             (UINTN)PageCount
             );
  ASSERT_RETURN_ERROR (Status);

  ZeroMem (ApicIds, EFI_PAGES_TO_SIZE ((UINTN)PageCount));

  PageCountValid = FALSE;

  CcExitVmgInit (Ghcb, &InterruptState);
  Ghcb->SaveArea.Rax = PageCount;
  CcExitVmgSetOffsetValid (Ghcb, GhcbRax);
  VmgExitStatus = CcExitVmgExit (Ghcb, SVM_EXIT_GET_APIC_IDS, (UINTN)ApicIds, 0);
  if (CcExitVmgIsOffsetValid (Ghcb, GhcbRax) && (Ghcb->SaveArea.Rax == PageCount)) {
    PageCountValid = TRUE;
  }

  CcExitVmgDone (Ghcb, InterruptState);

  ASSERT (VmgExitStatus == 0);
  ASSERT (PageCountValid);
  if ((VmgExitStatus != 0) || !PageCountValid) {
    FreePages (ApicIds, (UINTN)PageCount);
    return;
  }

  GuidData = (UINT64)(UINTN)ApicIds;
  BuildGuidDataHob (&gGhcbApicIdsGuid, &GuidData, sizeof (GuidData));
}

/**
  Initialize SEV-SNP support if running as an SEV-SNP guest.

**/
STATIC
VOID
AmdSevSnpInitialize (
  VOID
  )
{
  EFI_PEI_HOB_POINTERS         Hob;
  EFI_HOB_RESOURCE_DESCRIPTOR  *ResourceHob;
  UINT64                       HvFeatures;
  EFI_STATUS                   PcdStatus;

  if (!MemEncryptSevSnpIsEnabled ()) {
    return;
  }

  //
  // Query the hypervisor feature using the CcExitVmgExit and set the value in the
  // hypervisor features PCD.
  //
  HvFeatures = GetHypervisorFeature ();
  PcdStatus  = PcdSet64S (PcdGhcbHypervisorFeatures, HvFeatures);
  ASSERT_RETURN_ERROR (PcdStatus);

  //
  // Iterate through the system RAM and validate it.
  //
  for (Hob.Raw = GetHobList (); !END_OF_HOB_LIST (Hob); Hob.Raw = GET_NEXT_HOB (Hob)) {
    if ((Hob.Raw != NULL) && (GET_HOB_TYPE (Hob) == EFI_HOB_TYPE_RESOURCE_DESCRIPTOR)) {
      ResourceHob = Hob.ResourceDescriptor;

      if (ResourceHob->ResourceType == EFI_RESOURCE_SYSTEM_MEMORY) {
        if (ResourceHob->PhysicalStart >= SIZE_4GB) {
          ResourceHob->ResourceType = EFI_RESOURCE_MEMORY_UNACCEPTED;
          continue;
        }

        MemEncryptSevSnpPreValidateSystemRam (
          ResourceHob->PhysicalStart,
          EFI_SIZE_TO_PAGES ((UINTN)ResourceHob->ResourceLength)
          );
      }
    }
  }

  //
  // Retrieve the APIC IDs if the hypervisor supports it. These will be used
  // to always start APs using SNP AP Create.
  //
  if ((HvFeatures & GHCB_HV_FEATURES_APIC_ID_LIST) == GHCB_HV_FEATURES_APIC_ID_LIST) {
    AmdSevSnpGetApicIds ();
  }
}

/**
  Handle an SEV-SNP/GHCB protocol check failure.

  Notify the hypervisor using the VMGEXIT instruction that the SEV-SNP guest
  wishes to be terminated.

  @param[in] ReasonCode  Reason code to provide to the hypervisor for the
                         termination request.

**/
STATIC
VOID
SevEsProtocolFailure (
  IN UINT8  ReasonCode
  )
{
  MSR_SEV_ES_GHCB_REGISTER  Msr;

  //
  // Use the GHCB MSR Protocol to request termination by the hypervisor
  //
  Msr.GhcbPhysicalAddress         = 0;
  Msr.GhcbTerminate.Function      = GHCB_INFO_TERMINATE_REQUEST;
  Msr.GhcbTerminate.ReasonCodeSet = GHCB_TERMINATE_GHCB;
  Msr.GhcbTerminate.ReasonCode    = ReasonCode;
  AsmWriteMsr64 (MSR_SEV_ES_GHCB, Msr.GhcbPhysicalAddress);

  AsmVmgExit ();

  ASSERT (FALSE);
  CpuDeadLoop ();
}

/**
 Get the hypervisor features bitmap

**/
STATIC
UINT64
GetHypervisorFeature (
  VOID
  )
{
  UINT64                    Status;
  GHCB                      *Ghcb;
  MSR_SEV_ES_GHCB_REGISTER  Msr;
  BOOLEAN                   InterruptState;
  UINT64                    Features;

  Msr.GhcbPhysicalAddress = AsmReadMsr64 (MSR_SEV_ES_GHCB);
  Ghcb                    = Msr.Ghcb;

  //
  // Initialize the GHCB
  //
  CcExitVmgInit (Ghcb, &InterruptState);

  //
  // Query the Hypervisor Features.
  //
  Status = CcExitVmgExit (Ghcb, SVM_EXIT_HYPERVISOR_FEATURES, 0, 0);
  if ((Status != 0)) {
    SevEsProtocolFailure (GHCB_TERMINATE_GHCB_GENERAL);
  }

  Features = Ghcb->SaveArea.SwExitInfo2;

  CcExitVmgDone (Ghcb, InterruptState);

  return Features;
}

/**

  This function can be used to register the GHCB GPA.

  @param[in]  Address           The physical address to be registered.

**/
STATIC
VOID
GhcbRegister (
  IN  EFI_PHYSICAL_ADDRESS  Address
  )
{
  MSR_SEV_ES_GHCB_REGISTER  Msr;
  MSR_SEV_ES_GHCB_REGISTER  CurrentMsr;

  //
  // Save the current MSR Value
  //
  CurrentMsr.GhcbPhysicalAddress = AsmReadMsr64 (MSR_SEV_ES_GHCB);

  //
  // Use the GHCB MSR Protocol to request to register the GPA.
  //
  Msr.GhcbPhysicalAddress      = Address & ~EFI_PAGE_MASK;
  Msr.GhcbGpaRegister.Function = GHCB_INFO_GHCB_GPA_REGISTER_REQUEST;
  AsmWriteMsr64 (MSR_SEV_ES_GHCB, Msr.GhcbPhysicalAddress);

  AsmVmgExit ();

  Msr.GhcbPhysicalAddress = AsmReadMsr64 (MSR_SEV_ES_GHCB);

  //
  // If hypervisor responded with a different GPA than requested then fail.
  //
  if ((Msr.GhcbGpaRegister.Function != GHCB_INFO_GHCB_GPA_REGISTER_RESPONSE) ||
      ((Msr.GhcbPhysicalAddress & ~EFI_PAGE_MASK) != Address))
  {
    SevEsProtocolFailure (GHCB_TERMINATE_GHCB_GENERAL);
  }

  //
  // Restore the MSR
  //
  AsmWriteMsr64 (MSR_SEV_ES_GHCB, CurrentMsr.GhcbPhysicalAddress);
}

/**

  Initialize SEV-ES support if running as an SEV-ES guest.

  **/
STATIC
VOID
AmdSevEsInitialize (
  IN EFI_HOB_PLATFORM_INFO  *PlatformInfoHob
  )
{
  UINT8                *GhcbBase;
  PHYSICAL_ADDRESS     GhcbBasePa;
  UINTN                GhcbPageCount;
  UINT8                *GhcbBackupBase;
  UINT8                *GhcbBackupPages;
  UINTN                GhcbBackupPageCount;
  SEV_ES_PER_CPU_DATA  *SevEsData;
  UINTN                PageCount;
  RETURN_STATUS        Status;
  IA32_DESCRIPTOR      Gdtr;
  VOID                 *Gdt;

  if (!MemEncryptSevEsIsEnabled ()) {
    return;
  }

  Status = PcdSetBoolS (PcdSevEsIsEnabled, TRUE);
  ASSERT_RETURN_ERROR (Status);

  //
  // Allocate GHCB and per-CPU variable pages.
  //   Since the pages must survive across the UEFI to OS transition
  //   make them reserved.
  //
  GhcbPageCount = PlatformInfoHob->PcdCpuMaxLogicalProcessorNumber * 2;
  GhcbBase      = AllocateReservedPages (GhcbPageCount);
  ASSERT (GhcbBase != NULL);

  GhcbBasePa = (PHYSICAL_ADDRESS)(UINTN)GhcbBase;

  //
  // Each vCPU gets two consecutive pages, the first is the GHCB and the
  // second is the per-CPU variable page. Loop through the allocation and
  // only clear the encryption mask for the GHCB pages.
  //
  for (PageCount = 0; PageCount < GhcbPageCount; PageCount += 2) {
    Status = MemEncryptSevClearPageEncMask (
               0,
               GhcbBasePa + EFI_PAGES_TO_SIZE (PageCount),
               1
               );
    ASSERT_RETURN_ERROR (Status);
  }

  ZeroMem (GhcbBase, EFI_PAGES_TO_SIZE (GhcbPageCount));

  Status = PcdSet64S (PcdGhcbBase, GhcbBasePa);
  ASSERT_RETURN_ERROR (Status);

  PlatformInfoHob->GhcbBase = GhcbBasePa;

  Status = PcdSet64S (PcdGhcbSize, EFI_PAGES_TO_SIZE (GhcbPageCount));
  ASSERT_RETURN_ERROR (Status);

  DEBUG ((
    DEBUG_INFO,
    "SEV-ES is enabled, %lu GHCB pages allocated starting at 0x%p\n",
    (UINT64)GhcbPageCount,
    GhcbBase
    ));

  //
  // Allocate #VC recursion backup pages. The number of backup pages needed is
  // one less than the maximum VC count.
  //
  GhcbBackupPageCount = PlatformInfoHob->PcdCpuMaxLogicalProcessorNumber * (VMGEXIT_MAXIMUM_VC_COUNT - 1);
  GhcbBackupBase      = AllocatePages (GhcbBackupPageCount);
  ASSERT (GhcbBackupBase != NULL);

  GhcbBackupPages = GhcbBackupBase;
  for (PageCount = 1; PageCount < GhcbPageCount; PageCount += 2) {
    SevEsData =
      (SEV_ES_PER_CPU_DATA *)(GhcbBase + EFI_PAGES_TO_SIZE (PageCount));
    SevEsData->GhcbBackupPages = GhcbBackupPages;

    GhcbBackupPages += EFI_PAGE_SIZE * (VMGEXIT_MAXIMUM_VC_COUNT - 1);
  }

  DEBUG ((
    DEBUG_INFO,
    "SEV-ES is enabled, %lu GHCB backup pages allocated starting at 0x%p\n",
    (UINT64)GhcbBackupPageCount,
    GhcbBackupBase
    ));

  //
  // SEV-SNP guest requires that GHCB GPA must be registered before using it.
  //
  if (MemEncryptSevSnpIsEnabled ()) {
    GhcbRegister (GhcbBasePa);
  }

  AsmWriteMsr64 (MSR_SEV_ES_GHCB, GhcbBasePa);

  //
  // Now that the PEI GHCB is set up, the SEC GHCB page is no longer necessary
  // to keep shared. Later, it is exposed to the OS as EfiConventionalMemory, so
  // it needs to be marked private. The size of the region is hardcoded in
  // OvmfPkg/ResetVector/ResetVector.nasmb in the definition of
  // SNP_SEC_MEM_BASE_DESC_2.
  //
  Status = MemEncryptSevSetPageEncMask (
             0,                                  // Cr3 -- use system Cr3
             FixedPcdGet32 (PcdOvmfSecGhcbBase), // BaseAddress
             1                                   // NumPages
             );
  ASSERT_RETURN_ERROR (Status);

  //
  // The SEV support will clear the C-bit from non-RAM areas.  The early GDT
  // lives in a non-RAM area, so when an exception occurs (like a #VC) the GDT
  // will be read as un-encrypted even though it was created before the C-bit
  // was cleared (encrypted). This will result in a failure to be able to
  // handle the exception.
  //
  AsmReadGdtr (&Gdtr);

  Gdt = AllocatePages (EFI_SIZE_TO_PAGES ((UINTN)Gdtr.Limit + 1));
  ASSERT (Gdt != NULL);

  CopyMem (Gdt, (VOID *)Gdtr.Base, Gdtr.Limit + 1);
  Gdtr.Base = (UINTN)Gdt;
  AsmWriteGdtr (&Gdtr);
}

/**

  Function checks if SEV support is available, if present then it sets
  the dynamic PcdPteMemoryEncryptionAddressOrMask with memory encryption mask.

  **/
VOID
AmdSevInitialize (
  IN OUT EFI_HOB_PLATFORM_INFO  *PlatformInfoHob
  )
{
  UINT64         EncryptionMask;
  UINT64         CCGuestAttr;
  RETURN_STATUS  PcdStatus;

  //
  // Check if SEV is enabled
  //
  if (!MemEncryptSevIsEnabled ()) {
    return;
  }

  //
  // Check and perform SEV-SNP initialization if required. This need to be
  // done before the GHCB page is made shared in the AmdSevEsInitialize(). This
  // is because the system RAM must be validated before it is made shared.
  // The AmdSevSnpInitialize() validates the system RAM.
  //
  AmdSevSnpInitialize ();

  //
  // Set Memory Encryption Mask PCD
  //
  EncryptionMask = MemEncryptSevGetEncryptionMask ();
  PcdStatus      = PcdSet64S (PcdPteMemoryEncryptionAddressOrMask, EncryptionMask);
  ASSERT_RETURN_ERROR (PcdStatus);

  PlatformInfoHob->PteMemoryEncryptionAddressOrMask = EncryptionMask;

  DEBUG ((DEBUG_INFO, "SEV is enabled (mask 0x%lx)\n", EncryptionMask));

  //
  // Set Pcd to Deny the execution of option ROM when security
  // violation.
  //
  PcdStatus = PcdSet32S (PcdOptionRomImageVerificationPolicy, 0x4);
  ASSERT_RETURN_ERROR (PcdStatus);

  //
  // When SMM is required, cover the pages containing the initial SMRAM Save
  // State Map with a memory allocation HOB:
  //
  // There's going to be a time interval between our decrypting those pages for
  // SMBASE relocation and re-encrypting the same pages after SMBASE
  // relocation. We shall ensure that the DXE phase stay away from those pages
  // until after re-encryption, in order to prevent an information leak to the
  // hypervisor.
  //
  if (PlatformInfoHob->SmmSmramRequire && (PlatformInfoHob->BootMode != BOOT_ON_S3_RESUME)) {
    RETURN_STATUS  LocateMapStatus;
    UINTN          MapPagesBase;
    UINTN          MapPagesCount;

    LocateMapStatus = MemEncryptSevLocateInitialSmramSaveStateMapPages (
                        &MapPagesBase,
                        &MapPagesCount
                        );
    ASSERT_RETURN_ERROR (LocateMapStatus);

    if (PlatformInfoHob->Q35SmramAtDefaultSmbase) {
      //
      // The initial SMRAM Save State Map has been covered as part of a larger
      // reserved memory allocation in InitializeRamRegions().
      //
      ASSERT (SMM_DEFAULT_SMBASE <= MapPagesBase);
      ASSERT (
        (MapPagesBase + EFI_PAGES_TO_SIZE (MapPagesCount) <=
         SMM_DEFAULT_SMBASE + MCH_DEFAULT_SMBASE_SIZE)
        );
    } else {
      BuildMemoryAllocationHob (
        MapPagesBase,                      // BaseAddress
        EFI_PAGES_TO_SIZE (MapPagesCount), // Length
        EfiBootServicesData                // MemoryType
        );
    }
  }

  //
  // Check and perform SEV-ES initialization if required.
  //
  AmdSevEsInitialize (PlatformInfoHob);

  //
  // Set the Confidential computing attr PCD to communicate which SEV
  // technology is active.
  //
  if (MemEncryptSevSnpIsEnabled ()) {
    CCGuestAttr = CCAttrAmdSevSnp;
  } else if (MemEncryptSevEsIsEnabled ()) {
    CCGuestAttr = CCAttrAmdSevEs;
  } else {
    CCGuestAttr = CCAttrAmdSev;
  }

  if (MemEncryptSevEsDebugVirtualizationIsEnabled ()) {
    CCGuestAttr |= CCAttrFeatureAmdSevEsDebugVirtualization;
  }

  PcdStatus = PcdSet64S (PcdConfidentialComputingGuestAttr, CCGuestAttr);
  PlatformInfoHob->PcdConfidentialComputingGuestAttr = CCGuestAttr;

  ASSERT_RETURN_ERROR (PcdStatus);
}

/**
 The function performs SEV specific region initialization.

 **/
VOID
SevInitializeRam (
  VOID
  )
{
  if (MemEncryptSevSnpIsEnabled ()) {
    //
    // If SEV-SNP is enabled, reserve the Secrets and CPUID memory area.
    //
    // This memory range is given to the PSP by the hypervisor to populate
    // the information used during the SNP VM boots, and it need to persist
    // across the kexec boots. Mark it as EfiReservedMemoryType so that
    // the guest firmware and OS does not use it as a system memory.
    //
    BuildMemoryAllocationHob (
      (EFI_PHYSICAL_ADDRESS)(UINTN)PcdGet32 (PcdOvmfSnpSecretsBase),
      (UINT64)(UINTN)PcdGet32 (PcdOvmfSnpSecretsSize),
      EfiReservedMemoryType
      );
    BuildMemoryAllocationHob (
      (EFI_PHYSICAL_ADDRESS)(UINTN)PcdGet32 (PcdOvmfCpuidBase),
      (UINT64)(UINTN)PcdGet32 (PcdOvmfCpuidSize),
      EfiReservedMemoryType
      );

    //
    // The calling area memory needs to be protected until the OS can create
    // its own calling area. Mark it as EfiReservedMemoryType so that the
    // guest firmware and OS do not use it as a system memory.
    //
    BuildMemoryAllocationHob (
      (EFI_PHYSICAL_ADDRESS)(UINTN)PcdGet32 (PcdOvmfSecSvsmCaaBase),
      (UINT64)(UINTN)PcdGet32 (PcdOvmfSecSvsmCaaSize),
      EfiReservedMemoryType
      );
  }
}
