amd_iommu: Add helpers to walk AMD v1 Page Table format

The current amdvi_page_walk() is designed to be called by the replay()
method. Rather than drastically altering it, introduce helpers to fetch
guest PTEs that will be used by a page walker implementation.

Signed-off-by: Alejandro Jimenez <alejandro.j.jimenez@oracle.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Message-ID: <20250919213515.917111-8-alejandro.j.jimenez@oracle.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
This commit is contained in:
Alejandro Jimenez 2025-09-19 21:35:00 +00:00 committed by Michael S. Tsirkin
parent 0f13cc5642
commit a74bb3110a
2 changed files with 163 additions and 0 deletions

View file

@ -87,6 +87,8 @@ typedef enum AMDVIFaultReason {
AMDVI_FR_DTE_RTR_ERR = 1, /* Failure to retrieve DTE */
AMDVI_FR_DTE_V, /* DTE[V] = 0 */
AMDVI_FR_DTE_TV, /* DTE[TV] = 0 */
AMDVI_FR_PT_ROOT_INV, /* Page Table Root ptr invalid */
AMDVI_FR_PT_ENTRY_INV, /* Failure to read PTE from guest memory */
} AMDVIFaultReason;
uint64_t amdvi_extended_feature_register(AMDVIState *s)
@ -558,6 +560,127 @@ static int amdvi_as_to_dte(AMDVIAddressSpace *as, uint64_t *dte)
return 0;
}
/*
* For a PTE encoding a large page, return the page size it encodes as described
* by the AMD IOMMU Specification Table 14: Example Page Size Encodings.
* No need to adjust the value of the PTE to point to the first PTE in the large
* page since the encoding guarantees all "base" PTEs in the large page are the
* same.
*/
static uint64_t large_pte_page_size(uint64_t pte)
{
assert(PTE_NEXT_LEVEL(pte) == 7);
/* Determine size of the large/contiguous page encoded in the PTE */
return PTE_LARGE_PAGE_SIZE(pte);
}
/*
* Helper function to fetch a PTE using AMD v1 pgtable format.
* On successful page walk, returns 0 and pte parameter points to a valid PTE.
* On failure, returns:
* -AMDVI_FR_PT_ROOT_INV: A page walk is not possible due to conditions like DTE
* with invalid permissions, Page Table Root can not be read from DTE, or a
* larger IOVA than supported by page table level encoded in DTE[Mode].
* -AMDVI_FR_PT_ENTRY_INV: A PTE could not be read from guest memory during a
* page table walk. This means that the DTE has valid data, but one of the
* lower level entries in the Page Table could not be read.
*/
static int __attribute__((unused))
fetch_pte(AMDVIAddressSpace *as, hwaddr address, uint64_t dte, uint64_t *pte,
hwaddr *page_size)
{
IOMMUAccessFlags perms = amdvi_get_perms(dte);
uint8_t level, mode;
uint64_t pte_addr;
*pte = dte;
*page_size = 0;
if (perms == IOMMU_NONE) {
return -AMDVI_FR_PT_ROOT_INV;
}
/*
* The Linux kernel driver initializes the default mode to 3, corresponding
* to a 39-bit GPA space, where each entry in the pagetable translates to a
* 1GB (2^30) page size.
*/
level = mode = get_pte_translation_mode(dte);
assert(mode > 0 && mode < 7);
/*
* If IOVA is larger than the max supported by the current pgtable level,
* there is nothing to do.
*/
if (address > PT_LEVEL_MAX_ADDR(mode - 1)) {
/* IOVA too large for the current DTE */
return -AMDVI_FR_PT_ROOT_INV;
}
do {
level -= 1;
/* Update the page_size */
*page_size = PTE_LEVEL_PAGE_SIZE(level);
/* Permission bits are ANDed at every level, including the DTE */
perms &= amdvi_get_perms(*pte);
if (perms == IOMMU_NONE) {
return 0;
}
/* Not Present */
if (!IOMMU_PTE_PRESENT(*pte)) {
return 0;
}
/* Large or Leaf PTE found */
if (PTE_NEXT_LEVEL(*pte) == 7 || PTE_NEXT_LEVEL(*pte) == 0) {
/* Leaf PTE found */
break;
}
/*
* Index the pgtable using the IOVA bits corresponding to current level
* and walk down to the lower level.
*/
pte_addr = NEXT_PTE_ADDR(*pte, level, address);
*pte = amdvi_get_pte_entry(as->iommu_state, pte_addr, as->devfn);
if (*pte == (uint64_t)-1) {
/*
* A returned PTE of -1 indicates a failure to read the page table
* entry from guest memory.
*/
if (level == mode - 1) {
/* Failure to retrieve the Page Table from Root Pointer */
*page_size = 0;
return -AMDVI_FR_PT_ROOT_INV;
} else {
/* Failure to read PTE. Page walk skips a page_size chunk */
return -AMDVI_FR_PT_ENTRY_INV;
}
}
} while (level > 0);
assert(PTE_NEXT_LEVEL(*pte) == 0 || PTE_NEXT_LEVEL(*pte) == 7 ||
level == 0);
/*
* Page walk ends when Next Level field on PTE shows that either a leaf PTE
* or a series of large PTEs have been reached. In the latter case, even if
* the range starts in the middle of a contiguous page, the returned PTE
* must be the first PTE of the series.
*/
if (PTE_NEXT_LEVEL(*pte) == 7) {
/* Update page_size with the large PTE page size */
*page_size = large_pte_page_size(*pte);
}
return 0;
}
/* log error without aborting since linux seems to be using reserved bits */
static void amdvi_inval_devtab_entry(AMDVIState *s, uint64_t *cmd)
{

View file

@ -178,6 +178,46 @@
#define AMDVI_GATS_MODE (2ULL << 12)
#define AMDVI_HATS_MODE (2ULL << 10)
/* Page Table format */
#define AMDVI_PTE_PR (1ULL << 0)
#define AMDVI_PTE_NEXT_LEVEL_MASK GENMASK64(11, 9)
#define IOMMU_PTE_PRESENT(pte) ((pte) & AMDVI_PTE_PR)
/* Using level=0 for leaf PTE at 4K page size */
#define PT_LEVEL_SHIFT(level) (12 + ((level) * 9))
/* Return IOVA bit group used to index the Page Table at specific level */
#define PT_LEVEL_INDEX(level, iova) (((iova) >> PT_LEVEL_SHIFT(level)) & \
GENMASK64(8, 0))
/* Return the max address for a specified level i.e. max_oaddr */
#define PT_LEVEL_MAX_ADDR(x) (((x) < 5) ? \
((1ULL << PT_LEVEL_SHIFT((x + 1))) - 1) : \
(~(0ULL)))
/* Extract the NextLevel field from PTE/PDE */
#define PTE_NEXT_LEVEL(pte) (((pte) & AMDVI_PTE_NEXT_LEVEL_MASK) >> 9)
/* Take page table level and return default pagetable size for level */
#define PTE_LEVEL_PAGE_SIZE(level) (1ULL << (PT_LEVEL_SHIFT(level)))
/*
* Return address of lower level page table encoded in PTE and specified by
* current level and corresponding IOVA bit group at such level.
*/
#define NEXT_PTE_ADDR(pte, level, iova) (((pte) & AMDVI_DEV_PT_ROOT_MASK) + \
(PT_LEVEL_INDEX(level, iova) * 8))
/*
* Take a PTE value with mode=0x07 and return the page size it encodes.
*/
#define PTE_LARGE_PAGE_SIZE(pte) (1ULL << (1 + cto64(((pte) | 0xfffULL))))
/* Return number of PTEs to use for a given page size (expected power of 2) */
#define PAGE_SIZE_PTE_COUNT(pgsz) (1ULL << ((ctz64(pgsz) - 12) % 9))
/* IOTLB */
#define AMDVI_IOTLB_MAX_SIZE 1024
#define AMDVI_DEVID_SHIFT 36