/*++

Copyright (c) 2009, Hewlett-Packard Company. All rights reserved.<BR>
Portions copyright (c) 2010, Apple Inc. All rights reserved.<BR>
Portions copyright (c) 2011-2021, Arm Limited. All rights reserved.<BR>
Copyright (c) 2017, Intel Corporation. All rights reserved.<BR>

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


--*/

#include <Library/MemoryAllocationLib.h>
#include "CpuDxe.h"

#define INVALID_ENTRY  ((UINT64)~0)

#define MIN_T0SZ        16
#define BITS_PER_LEVEL  9

/**
  Parses T0SZ to determine the level and number of entries at the root
  of the translation table.

  @param T0SZ                 The T0SZ value to be parsed.
  @param RootTableLevel       The level of the root table.
  @param RootTableEntryCount  The number of entries in the root table.
**/
STATIC
VOID
GetRootTranslationTableInfo (
  IN  UINTN  T0SZ,
  OUT UINTN  *RootTableLevel,
  OUT UINTN  *RootTableEntryCount
  )
{
  *RootTableLevel      = (T0SZ - MIN_T0SZ) / BITS_PER_LEVEL;
  *RootTableEntryCount = TT_ENTRY_COUNT >> (T0SZ - MIN_T0SZ) % BITS_PER_LEVEL;
}

/**
  Converts ARM translation table attributes to GCD attributes.

  @param PageAttributes The translation table attributes to be converted.

  @retval The analogous GCD attributes.
**/
STATIC
UINT64
PageAttributeToGcdAttribute (
  IN UINT64  PageAttributes
  )
{
  UINT64  GcdAttributes;

  switch (PageAttributes & TT_ATTR_INDX_MASK) {
    case TT_ATTR_INDX_DEVICE_MEMORY:
      GcdAttributes = EFI_MEMORY_UC;
      break;
    case TT_ATTR_INDX_MEMORY_NON_CACHEABLE:
      GcdAttributes = EFI_MEMORY_WC;
      break;
    case TT_ATTR_INDX_MEMORY_WRITE_THROUGH:
      GcdAttributes = EFI_MEMORY_WT;
      break;
    case TT_ATTR_INDX_MEMORY_WRITE_BACK:
      GcdAttributes = EFI_MEMORY_WB;
      break;
    default:
      DEBUG ((
        DEBUG_ERROR,
        "PageAttributeToGcdAttribute: PageAttributes:0x%lX not supported.\n",
        PageAttributes
        ));
      ASSERT (0);
      // The Global Coherency Domain (GCD) value is defined as a bit set.
      // Returning 0 means no attribute has been set.
      GcdAttributes = 0;
  }

  // Determine protection attributes
  if ((PageAttributes & TT_AF) == 0) {
    GcdAttributes |= EFI_MEMORY_RP;
  }

  if (((PageAttributes & TT_AP_MASK) == TT_AP_NO_RO) ||
      ((PageAttributes & TT_AP_MASK) == TT_AP_RO_RO))
  {
    // Read only cases map to write-protect
    GcdAttributes |= EFI_MEMORY_RO;
  }

  // Process eXecute Never attribute
  if ((PageAttributes & (TT_PXN_MASK | TT_UXN_MASK)) != 0) {
    GcdAttributes |= EFI_MEMORY_XP;
  }

  return GcdAttributes;
}

/**
  Convert an arch specific set of page attributes into a mask
  of EFI_MEMORY_xx constants.

  @param  PageAttributes  The set of page attributes.

  @retval The mask of EFI_MEMORY_xx constants.

**/
UINT64
RegionAttributeToGcdAttribute (
  IN UINTN  PageAttributes
  )
{
  return PageAttributeToGcdAttribute (PageAttributes);
}

/**
  Retrieves the attribute of the first page entry in the translation table.

  @param[in] FirstLevelTableAddress   The base address of the translation table.
  @param[in] TableLevel               The current level being traversed.

  @retval The attributes of the first page entry found, or INVALID_ENTRY.
**/
STATIC
UINT64
GetFirstPageAttribute (
  IN UINT64  *FirstLevelTableAddress,
  IN UINTN   TableLevel
  )
{
  UINT64  FirstEntry;

  // Get the first entry of the table
  FirstEntry = *FirstLevelTableAddress;

  if ((TableLevel != 3) && ((FirstEntry & TT_TYPE_MASK) == TT_TYPE_TABLE_ENTRY)) {
    // Only valid for Levels 0, 1 and 2

    // Get the attribute of the subsequent table
    return GetFirstPageAttribute ((UINT64 *)(FirstEntry & TT_ADDRESS_MASK_DESCRIPTION_TABLE), TableLevel + 1);
  } else if (((FirstEntry & TT_TYPE_MASK) == TT_TYPE_BLOCK_ENTRY) ||
             ((TableLevel == 3) && ((FirstEntry & TT_TYPE_MASK) == TT_TYPE_BLOCK_ENTRY_LEVEL3)))
  {
    return FirstEntry & TT_ATTRIBUTES_MASK;
  } else {
    return INVALID_ENTRY;
  }
}

/**
  This function recursively traverses the translation table heirarchy to
  synchronise the GCD with the translation table.

  @param[in]        TableAddress        The address of the table being processed.
  @param[in]        EntryCount          The number of entries in the current level of the table.
  @param[in]        TableLevel          The current level of the memory table being processed.
  @param[in]        BaseAddress         The starting address of the region.
  @param[in, out]   PrevEntryAttribute  The attributes of the previous region.
  @param[in, out]   StartGcdRegion      The start of the GCD region.

  @retval The address at the end of the last region processed.
**/
STATIC
UINT64
GetNextEntryAttribute (
  IN     UINT64  *TableAddress,
  IN     UINTN   EntryCount,
  IN     UINTN   TableLevel,
  IN     UINT64  BaseAddress,
  IN OUT UINT64  *PrevEntryAttribute,
  IN OUT UINT64  *StartGcdRegion
  )
{
  UINTN                            Index;
  UINT64                           Entry;
  UINT64                           EntryAttribute;
  UINT64                           EntryType;
  EFI_STATUS                       Status;
  UINTN                            NumberOfDescriptors;
  EFI_GCD_MEMORY_SPACE_DESCRIPTOR  *MemorySpaceMap;

  // Get the memory space map from GCD
  MemorySpaceMap = NULL;
  Status         = gDS->GetMemorySpaceMap (&NumberOfDescriptors, &MemorySpaceMap);

  if (EFI_ERROR (Status) || (TableLevel > 3)) {
    ASSERT_EFI_ERROR (Status);
    ASSERT (TableLevel <= 3);
    return 0;
  }

  // While the top level table might not contain TT_ENTRY_COUNT entries;
  // the subsequent ones should be filled up
  for (Index = 0; Index < EntryCount; Index++) {
    Entry          = TableAddress[Index];
    EntryType      = Entry & TT_TYPE_MASK;
    EntryAttribute = Entry & TT_ATTRIBUTES_MASK;

    // If Entry is a Table Descriptor type entry then go through the sub-level table
    if ((EntryType == TT_TYPE_BLOCK_ENTRY) ||
        ((TableLevel == 3) && (EntryType == TT_TYPE_BLOCK_ENTRY_LEVEL3)))
    {
      if ((*PrevEntryAttribute == INVALID_ENTRY) || (EntryAttribute != *PrevEntryAttribute)) {
        if (*PrevEntryAttribute != INVALID_ENTRY) {
          // Update GCD with the last region
          SetGcdMemorySpaceAttributes (
            MemorySpaceMap,
            NumberOfDescriptors,
            *StartGcdRegion,
            (BaseAddress + (Index * TT_ADDRESS_AT_LEVEL (TableLevel))) - *StartGcdRegion,
            PageAttributeToGcdAttribute (*PrevEntryAttribute)
            );
        }

        // Start of the new region
        *StartGcdRegion     = BaseAddress + (Index * TT_ADDRESS_AT_LEVEL (TableLevel));
        *PrevEntryAttribute = EntryAttribute;
      } else {
        continue;
      }
    } else if (EntryType == TT_TYPE_TABLE_ENTRY) {
      // Table Entry type is only valid for Level 0, 1, 2
      ASSERT (TableLevel < 3);

      // Increase the level number and scan the sub-level table
      GetNextEntryAttribute (
        (UINT64 *)(Entry & TT_ADDRESS_MASK_DESCRIPTION_TABLE),
        TT_ENTRY_COUNT,
        TableLevel + 1,
        (BaseAddress + (Index * TT_ADDRESS_AT_LEVEL (TableLevel))),
        PrevEntryAttribute,
        StartGcdRegion
        );
    } else {
      if (*PrevEntryAttribute != INVALID_ENTRY) {
        // Update GCD with the last region
        SetGcdMemorySpaceAttributes (
          MemorySpaceMap,
          NumberOfDescriptors,
          *StartGcdRegion,
          (BaseAddress + (Index * TT_ADDRESS_AT_LEVEL (TableLevel))) - *StartGcdRegion,
          PageAttributeToGcdAttribute (*PrevEntryAttribute)
          );

        // Start of the new region
        *StartGcdRegion     = BaseAddress + (Index * TT_ADDRESS_AT_LEVEL (TableLevel));
        *PrevEntryAttribute = INVALID_ENTRY;
      }
    }
  }

  FreePool (MemorySpaceMap);

  return BaseAddress + (EntryCount * TT_ADDRESS_AT_LEVEL (TableLevel));
}

/**
  Sync the GCD memory space attributes with the translation table.

  @param[in]  CpuProtocol The CPU architectural protocol instance.

  @retval EFI_SUCCESS The GCD memory space attributes are synced with
                      the MMU page table.
  @retval Others      The return value of GetMemorySpaceMap().
**/
EFI_STATUS
SyncCacheConfig (
  IN  EFI_CPU_ARCH_PROTOCOL  *CpuProtocol
  )
{
  EFI_STATUS                       Status;
  UINT64                           PageAttribute;
  UINT64                           *FirstLevelTableAddress;
  UINTN                            TableLevel;
  UINTN                            TableCount;
  UINTN                            NumberOfDescriptors;
  EFI_GCD_MEMORY_SPACE_DESCRIPTOR  *MemorySpaceMap;
  UINTN                            Tcr;
  UINTN                            T0SZ;
  UINT64                           BaseAddressGcdRegion;
  UINT64                           EndAddressGcdRegion;

  // This code assumes MMU is enabled and filed with section translations
  ASSERT (ArmMmuEnabled ());

  //
  // Get the memory space map from GCD
  //
  MemorySpaceMap = NULL;
  Status         = gDS->GetMemorySpaceMap (&NumberOfDescriptors, &MemorySpaceMap);

  if (EFI_ERROR (Status)) {
    ASSERT_EFI_ERROR (Status);
    return Status;
  }

  // The GCD implementation maintains its own copy of the state of memory space attributes.  GCD needs
  // to know what the initial memory space attributes are.  The CPU Arch. Protocol does not provide a
  // GetMemoryAttributes function for GCD to get this so we must resort to calling GCD (as if we were
  // a client) to update its copy of the attributes.  This is bad architecture and should be replaced
  // with a way for GCD to query the CPU Arch. driver of the existing memory space attributes instead.

  // Obtain page table base
  FirstLevelTableAddress = (UINT64 *)(ArmGetTTBR0BaseAddress ());

  // Get Translation Control Register value
  Tcr = ArmGetTCR ();
  // Get Address Region Size
  T0SZ = Tcr & TCR_T0SZ_MASK;

  // Get the level of the first table for the indicated Address Region Size
  GetRootTranslationTableInfo (T0SZ, &TableLevel, &TableCount);

  // First Attribute of the Page Tables
  PageAttribute = GetFirstPageAttribute (FirstLevelTableAddress, TableLevel);

  // We scan from the start of the memory map (ie: at the address 0x0)
  BaseAddressGcdRegion = 0x0;
  EndAddressGcdRegion  = GetNextEntryAttribute (
                           FirstLevelTableAddress,
                           TableCount,
                           TableLevel,
                           BaseAddressGcdRegion,
                           &PageAttribute,
                           &BaseAddressGcdRegion
                           );

  // Update GCD with the last region if valid
  if ((PageAttribute != INVALID_ENTRY) && (EndAddressGcdRegion > BaseAddressGcdRegion)) {
    SetGcdMemorySpaceAttributes (
      MemorySpaceMap,
      NumberOfDescriptors,
      BaseAddressGcdRegion,
      EndAddressGcdRegion - BaseAddressGcdRegion,
      PageAttributeToGcdAttribute (PageAttribute)
      );
  }

  FreePool (MemorySpaceMap);

  return EFI_SUCCESS;
}

/**
  Convert EFI memory attributes to ARM translation table attributes.

  @param[in]  EfiAttributes  EFI memory attributes.

  @retval The analogous translation table attributes.
**/
UINT64
EfiAttributeToArmAttribute (
  IN UINT64  EfiAttributes
  )
{
  UINT64  ArmAttributes;

  switch (EfiAttributes & EFI_MEMORY_CACHETYPE_MASK) {
    case EFI_MEMORY_UC:
      if (ArmReadCurrentEL () == AARCH64_EL2) {
        ArmAttributes = TT_ATTR_INDX_DEVICE_MEMORY | TT_XN_MASK;
      } else {
        ArmAttributes = TT_ATTR_INDX_DEVICE_MEMORY | TT_UXN_MASK | TT_PXN_MASK;
      }

      break;
    case EFI_MEMORY_WC:
      ArmAttributes = TT_ATTR_INDX_MEMORY_NON_CACHEABLE;
      break;
    case EFI_MEMORY_WT:
      ArmAttributes = TT_ATTR_INDX_MEMORY_WRITE_THROUGH | TT_SH_INNER_SHAREABLE;
      break;
    case EFI_MEMORY_WB:
      ArmAttributes = TT_ATTR_INDX_MEMORY_WRITE_BACK | TT_SH_INNER_SHAREABLE;
      break;
    default:
      ArmAttributes = TT_ATTR_INDX_MASK;
  }

  // Set the access flag to match the block attributes
  if ((EfiAttributes & EFI_MEMORY_RP) == 0) {
    ArmAttributes |= TT_AF;
  }

  // Determine protection attributes
  if ((EfiAttributes & EFI_MEMORY_RO) != 0) {
    ArmAttributes |= TT_AP_NO_RO;
  }

  // Process eXecute Never attribute
  if ((EfiAttributes & EFI_MEMORY_XP) != 0) {
    ArmAttributes |= TT_PXN_MASK;
  }

  return ArmAttributes;
}

/**
  This function returns the attributes of the memory region containing the
  specified address.

  RegionLength and RegionAttributes are only valid if the result is EFI_SUCCESS.

  @param[in]        TranslationTable  The translation table base address.
  @param[in]        TableLevel        The level of the translation table.
  @param[in]        LastBlockEntry    The last block address of the table level.
  @param[in, out]   BaseAddress       The base address of the memory region.
  @param[out]       RegionLength      The length of the memory region.
  @param[out]       RegionAttributes  The attributes of the memory region.

  @retval EFI_SUCCESS     The attributes of the memory region were
                          returned successfully.
  @retval EFI_NOT_FOUND   The memory region was not found.
  @retval EFI_NO_MAPPING  The translation table entry associated with
                          BaseAddress is invalid.
**/
EFI_STATUS
GetMemoryRegionRec (
  IN     UINT64  *TranslationTable,
  IN     UINTN   TableLevel,
  IN     UINT64  *LastBlockEntry,
  IN OUT UINTN   *BaseAddress,
  OUT    UINTN   *RegionLength,
  OUT    UINTN   *RegionAttributes
  )
{
  EFI_STATUS  Status;
  UINT64      *NextTranslationTable;
  UINT64      *BlockEntry;
  UINT64      BlockEntryType;
  UINT64      EntryType;

  if (TableLevel != 3) {
    BlockEntryType = TT_TYPE_BLOCK_ENTRY;
  } else {
    BlockEntryType = TT_TYPE_BLOCK_ENTRY_LEVEL3;
  }

  // Find the block entry linked to the Base Address
  BlockEntry = (UINT64 *)TT_GET_ENTRY_FOR_ADDRESS (TranslationTable, TableLevel, *BaseAddress);
  EntryType  = *BlockEntry & TT_TYPE_MASK;

  if ((TableLevel < 3) && (EntryType == TT_TYPE_TABLE_ENTRY)) {
    NextTranslationTable = (UINT64 *)(*BlockEntry & TT_ADDRESS_MASK_DESCRIPTION_TABLE);

    // The entry is a page table, so we go to the next level
    Status = GetMemoryRegionRec (
               NextTranslationTable, // Address of the next level page table
               TableLevel + 1,       // Next Page Table level
               (UINTN *)TT_LAST_BLOCK_ADDRESS (NextTranslationTable, TT_ENTRY_COUNT),
               BaseAddress,
               RegionLength,
               RegionAttributes
               );

    // EFI_SUCCESS:     The end of the end of the region was found.
    // EFI_NO_MAPPING:  The translation entry associated with BaseAddress is invalid.
    if (Status != EFI_NOT_FOUND) {
      return Status;
    }

    // Now we processed the table move to the next entry
    BlockEntry++;
  } else if (EntryType == BlockEntryType) {
    // We have found the BlockEntry attached to the address. We save its start address (the start
    // address might be before the 'BaseAddress') and attributes
    *BaseAddress      = *BaseAddress & ~(TT_ADDRESS_AT_LEVEL (TableLevel) - 1);
    *RegionLength     = 0;
    *RegionAttributes = *BlockEntry & TT_ATTRIBUTES_MASK;
  } else {
    return EFI_NO_MAPPING;
  }

  while (BlockEntry <= LastBlockEntry) {
    if (((*BlockEntry & TT_TYPE_MASK) == BlockEntryType) &&
        ((*BlockEntry & TT_ATTRIBUTES_MASK) == *RegionAttributes))
    {
      *RegionLength = *RegionLength + TT_BLOCK_ENTRY_SIZE_AT_LEVEL (TableLevel);
    } else {
      // In case we have found the end of the region we return success
      return EFI_SUCCESS;
    }

    BlockEntry++;
  }

  // If we have reached the end of the TranslationTable and we have not found the end of the region then
  // we return EFI_NOT_FOUND.
  // The caller will continue to look for the memory region at its level.
  return EFI_NOT_FOUND;
}

/**
  Retrieves a memory region from a given base address.

  This function retrieves a memory region starting from a given base address.

  @param[in, out] BaseAddress       The base address from which to retrieve
                                    the memory region. On successful return, this is
                                    updated to the end address of the retrieved region.
  @param[out]     RegionLength      The length of the retrieved memory region.
  @param[out]     RegionAttributes  The attributes of the retrieved memory region.

  @retval EFI_STATUS              Returns EFI_SUCCESS if the memory region is
                                  retrieved successfully, or the status of the
                                  recursive call to GetMemoryRegionRec.
  @retval EFI_NOT_FOUND           The memory region was not found.
  @retval EFI_NO_MAPPING          The translation table entry associated with
                                  BaseAddress is invalid.
  @retval EFI_INVALID_PARAMETER   One of the input parameters was NULL.
**/
EFI_STATUS
GetMemoryRegion (
  IN OUT UINTN  *BaseAddress,
  OUT    UINTN  *RegionLength,
  OUT    UINTN  *RegionAttributes
  )
{
  EFI_STATUS  Status;
  UINT64      *TranslationTable;
  UINTN       TableLevel;
  UINTN       EntryCount;
  UINTN       T0SZ;

  if ((BaseAddress == NULL) || (RegionLength == NULL) || (RegionAttributes == NULL)) {
    ASSERT ((BaseAddress != NULL) && (RegionLength != NULL) && (RegionAttributes != NULL));
    return EFI_INVALID_PARAMETER;
  }

  TranslationTable = ArmGetTTBR0BaseAddress ();

  // Initialize the output parameters. These paramaters are only valid if the
  // result is EFI_SUCCESS.
  *RegionLength     = 0;
  *RegionAttributes = 0;

  T0SZ = ArmGetTCR () & TCR_T0SZ_MASK;
  // Get the Table info from T0SZ
  GetRootTranslationTableInfo (T0SZ, &TableLevel, &EntryCount);

  Status = GetMemoryRegionRec (
             TranslationTable,
             TableLevel,
             (UINTN *)TT_LAST_BLOCK_ADDRESS (TranslationTable, EntryCount),
             BaseAddress,
             RegionLength,
             RegionAttributes
             );

  // If the region continues up to the end of the root table then GetMemoryRegionRec()
  // will return EFI_NOT_FOUND. Check if the region length was updated.
  if ((Status == EFI_NOT_FOUND) && (*RegionLength > 0)) {
    return EFI_SUCCESS;
  }

  return Status;
}
