Nested page table entries that were touched during nested page table walks for guest PTEs always have their dirty and accessed bits set. Write a test that verifies this behavior for guest read and writes. Note that non-leaf NPT levels encountered during the GPA to HPA translation for guest PTEs only have their accessed bits set. The nVMX tests already have coverage for TDP A/D bits. Add a similar test for nSVM to improve test parity between nSVM and nVMX. Signed-off-by: Kevin Cheng --- x86/svm_npt.c | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/x86/svm_npt.c b/x86/svm_npt.c index bd5e8f351e343..e436c43fb1c4c 100644 --- a/x86/svm_npt.c +++ b/x86/svm_npt.c @@ -380,6 +380,181 @@ skip_pte_test: vmcb->save.cr4 = sg_cr4; } + +static void clear_npt_ad_pte(unsigned long *pml4e, void *gpa) +{ + unsigned long *pte; + int l; + + for (l = PAGE_LEVEL; l > 0; --l) { + pte = get_pte_level(pml4e, gpa, l); + *pte &= ~(PT_AD_MASK); + if (*pte & PT_PAGE_SIZE_MASK) + break; + } +} + +/* + * clear_npt_ad : Clear NPT A/D bits for the page table walk and the final + * GPA of a guest address. + */ +static void clear_npt_ad(u64 *pml4e, unsigned long *guest_cr3, + void *gva) +{ + unsigned long *pte = guest_cr3, gpa; + u64 offset_in_page; + int l; + + for (l = PAGE_LEVEL; l > 0; --l) { + pte = get_pte_level(guest_cr3, gva, l); + + clear_npt_ad_pte(pml4e, (void *)pte); + + assert(*pte & PT_PRESENT_MASK); + if (*pte & PT_PAGE_SIZE_MASK) + break; + } + + pte = get_pte_level(guest_cr3, gva, l); + offset_in_page = (u64)gva & ((1 << PGDIR_BITS(l)) - 1); + gpa = (*pte & PT_ADDR_MASK) | ((u64)gva & offset_in_page); + clear_npt_ad_pte(pml4e, (void *)gpa); +} + +/* + * check_npt_ad_pte : Check the NPT A/D bits at each level for GPA being + * translated. Note that non-leaf NPT levels encountered during translation + * only have the access bit set. + */ +static bool check_npt_ad_pte(u64 *pml4e, void *gpa, int guest_level, + int expected_ad) +{ + int l, expected_ad_level; + unsigned long *pte; + bool leaf; + + for (l = PAGE_LEVEL; l > 0; --l) { + pte = get_pte_level(pml4e, gpa, l); + leaf = (l == 1) || (*pte & PT_PAGE_SIZE_MASK); + expected_ad_level = expected_ad; + + /* The dirty bit is only set on leaf PTEs */ + if (!leaf) + expected_ad_level = expected_ad & ~PT_DIRTY_MASK; + + if ((*pte & PT_AD_MASK) != expected_ad_level) { + report_fail("NPT - guest level %d npt level %d page table received: A=%d/D=%d, expected A=%d/D=%d", + guest_level, + l, + !!(*pte & PT_ACCESSED_MASK), + !!(*pte & PT_DIRTY_MASK), + !!(expected_ad & PT_ACCESSED_MASK), + !!(expected_ad & PT_DIRTY_MASK)); + return true; + } + + if (leaf) + break; + } + + return false; +} + +/* + * check_npt_ad : Check the content of NPT A/D bits for the page table walk + * and the final GPA of a guest address. + */ +static void check_npt_ad(u64 *pml4e, unsigned long *guest_cr3, + void *gva, int expected_gpa_ad) +{ + unsigned long *pte = guest_cr3, gpa; + u64 *npt_pte, offset_in_page; + bool bad_pt_ad = false; + int l; + + for (l = PAGE_LEVEL; l > 0; --l) { + pte = get_pte_level(guest_cr3, gva, l); + npt_pte = npt_get_pte((u64) pte); + + if (!npt_pte) { + report_fail("NPT - guest level %d page table is not mapped.\n", l); + return; + } + + if (!bad_pt_ad) + bad_pt_ad |= check_npt_ad_pte(pml4e, (void *)pte, l, PT_AD_MASK); + + assert(*pte & PT_PRESENT_MASK); + if (*pte & PT_PAGE_SIZE_MASK) + break; + } + + pte = get_pte_level(guest_cr3, gva, l); + offset_in_page = (u64)gva & ((1 << PGDIR_BITS(l)) - 1); + gpa = (*pte & PT_ADDR_MASK) | ((u64)gva & offset_in_page); + + npt_pte = npt_get_pte(gpa); + + if (!npt_pte) { + report_fail("NPT - guest physical address is not mapped"); + return; + } + + check_npt_ad_pte(pml4e, (void *)gpa, l, expected_gpa_ad); + report((*npt_pte & PT_AD_MASK) == expected_gpa_ad, + "NPT - guest physical address received: A=%d/D=%d, expected A=%d/D=%d", + !!(*npt_pte & PT_ACCESSED_MASK), + !!(*npt_pte & PT_DIRTY_MASK), + !!(expected_gpa_ad & PT_ACCESSED_MASK), + !!(expected_gpa_ad & PT_DIRTY_MASK)); +} + +static void npt_ad_read_guest(struct svm_test *test) +{ + (void)*(volatile u64 *)scratch_page; +} + +static void npt_ad_write_guest(struct svm_test *test) +{ + *((u64 *)scratch_page) = 42; +} + +static void npt_ad_test(void) +{ + unsigned long *guest_cr3 = (unsigned long *) vmcb->save.cr3; + + if (!npt_supported()) { + report_skip("NPT not supported"); + return; + } + + scratch_page = alloc_page(); + + clear_npt_ad(npt_get_pml4e(), guest_cr3, scratch_page); + + test_set_guest(npt_ad_read_guest); + svm_vmrun(); + + /* + * NPT walks for guest page tables are write accesses by default unless + * read-only guest page tables are used. As a result, we expect the + * dirty bit to be set on NPT mappings of guest page tables. Since the + * access itself is a read, we expect the final translation to not have + * the dirty bit set. + */ + check_npt_ad(npt_get_pml4e(), guest_cr3, scratch_page, PT_ACCESSED_MASK); + + test_set_guest(npt_ad_write_guest); + svm_vmrun(); + + check_npt_ad(npt_get_pml4e(), guest_cr3, scratch_page, PT_AD_MASK); + + report(*((u64 *)scratch_page) == 42, "Expected: 42, received: %ld", + *((u64 *)scratch_page)); + + clear_npt_ad(npt_get_pml4e(), guest_cr3, scratch_page); +} + #define NPT_V1_TEST(name, prepare, guest_code, check) \ { #name, npt_supported, prepare, default_prepare_gif_clear, guest_code, \ default_finished, check } @@ -395,6 +570,7 @@ static struct svm_test npt_tests[] = { NPT_V1_TEST(npt_l1mmio, npt_l1mmio_prepare, npt_l1mmio_test, npt_l1mmio_check), NPT_V1_TEST(npt_rw_l1mmio, npt_rw_l1mmio_prepare, npt_rw_l1mmio_test, npt_rw_l1mmio_check), NPT_V2_TEST(svm_npt_rsvd_bits_test), + NPT_V2_TEST(npt_ad_test), { NULL, NULL, NULL, NULL, NULL, NULL, NULL } }; -- 2.52.0.322.g1dd061c0dc-goog