[PATCH v2] hw/riscv/riscv-iommu: encode debug translation ranges

Zephyr Li posted 1 patch 12 hours ago
hw/riscv/riscv-iommu.c         |  14 ++-
tests/qtest/iommu-riscv-test.c | 193 +++++++++++++++++++++++++++++++++
2 files changed, 206 insertions(+), 1 deletion(-)
[PATCH v2] hw/riscv/riscv-iommu: encode debug translation ranges
Posted by Zephyr Li 12 hours ago
The debug translation response aligns the translated physical address to
the effective translation range, but always reports S=0. For a request
inside a superpage, the response therefore identifies the wrong 4 KiB
physical page.

For effective ranges larger than 4 KiB, set S and encode the range size
using trailing ones in the PPN field. Keep the existing response format
for 4 KiB translations.

The RISC-V IOMMU specification permits reporting a 4 KiB translation even
when the page tables use superpages. Reporting the effective range is an
implementation choice; this change does not claim that S=1 is mandatory
for every translation through a superpage mapping.

Add debug-response tests for S-stage-only 4 KiB, 64 KiB NAPOT, and 2 MiB
translations, G-stage-only 2 MiB translations, and nested S4K/G2M,
S2M/G4K, and S2M/G2M mappings. Check requests inside large ranges and at
both ends. The mixed nested cases verify that the response uses the
smaller effective translation granularity.

Fixes: a7aa525b93c3 ("hw/riscv/riscv-iommu: add DBG support")
Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3552
Signed-off-by: Zephyr Li <fritchleybohrer@gmail.com>
---
Changes in v2:
- Regenerate the patch from the source tree to correct malformed hunk metadata.
- No functional changes.

---
 hw/riscv/riscv-iommu.c         |  14 ++-
 tests/qtest/iommu-riscv-test.c | 193 +++++++++++++++++++++++++++++++++
 2 files changed, 206 insertions(+), 1 deletion(-)

diff --git a/hw/riscv/riscv-iommu.c b/hw/riscv/riscv-iommu.c
index 323a041b4a..67d8456b9d 100644
--- a/hw/riscv/riscv-iommu.c
+++ b/hw/riscv/riscv-iommu.c
@@ -2281,8 +2281,20 @@ static void riscv_iommu_process_dbg(RISCVIOMMUState *s)
         if (fault) {
             iova = RISCV_IOMMU_TR_RESPONSE_FAULT | (((uint64_t) fault) << 10);
         } else {
+            uint64_t ppn;
+
             iova = iotlb.translated_addr & ~iotlb.addr_mask;
-            iova = set_field(0, RISCV_IOMMU_TR_RESPONSE_PPN, PPN_DOWN(iova));
+            ppn = PPN_DOWN(iova);
+
+            if (iotlb.addr_mask > TARGET_PAGE_SIZE - 1) {
+                /* Encode the translation range as trailing ones in the PPN. */
+                ppn |= PPN_DOWN(iotlb.addr_mask) >> 1;
+                iova = RISCV_IOMMU_TR_RESPONSE_S;
+            } else {
+                iova = 0;
+            }
+
+            iova = set_field(iova, RISCV_IOMMU_TR_RESPONSE_PPN, ppn);
         }
         riscv_iommu_reg_set64(s, RISCV_IOMMU_REG_TR_RESPONSE, iova);
     }
diff --git a/tests/qtest/iommu-riscv-test.c b/tests/qtest/iommu-riscv-test.c
index 2638024891..1cd753c4f2 100644
--- a/tests/qtest/iommu-riscv-test.c
+++ b/tests/qtest/iommu-riscv-test.c
@@ -19,6 +19,12 @@
 
 #define DMA_LEN           4
 
+#define DBG_PAGE_SIZE           0x1000ull
+#define DBG_NAPOT_SIZE          0x10000ull
+#define DBG_SUPERPAGE_SIZE      0x200000ull
+#define DBG_TRANSLATED_PA       0x90000000ull
+#define DBG_NESTED_GPA          0x200000ull
+
 /* RISC-V virt machine PCI configuration */
 #define RISCV_GPEX_PIO_BASE        0x3000000
 #define RISCV_BUS_PIO_LIMIT        0x10000
@@ -35,6 +41,64 @@ typedef struct RiscvIommuTestState {
     uint64_t iommu_base;
 } RiscvIommuTestState;
 
+typedef struct RiscvIommuDbgTest {
+    const char *name;
+    QRIOMMUTransMode mode;
+    uint64_t s_page_size;
+    uint64_t g_page_size;
+    uint64_t pa;
+    uint64_t expected_response;
+} RiscvIommuDbgTest;
+
+static const RiscvIommuDbgTest debug_tests[] = {
+    {
+        .name = "page-4k",
+        .mode = QRIOMMU_TM_S_STAGE_ONLY,
+        .s_page_size = DBG_PAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA + DBG_PAGE_SIZE,
+        .expected_response = 0x24000400ull,
+    }, {
+        .name = "napot-64k",
+        .mode = QRIOMMU_TM_S_STAGE_ONLY,
+        .s_page_size = DBG_NAPOT_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x24001e00ull,
+    }, {
+        .name = "superpage",
+        .mode = QRIOMMU_TM_S_STAGE_ONLY,
+        .s_page_size = DBG_SUPERPAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x2403fe00ull,
+    }, {
+        .name = "g-stage-superpage",
+        .mode = QRIOMMU_TM_G_STAGE_ONLY,
+        .g_page_size = DBG_SUPERPAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x2403fe00ull,
+    }, {
+        .name = "nested-s4k-g2m",
+        .mode = QRIOMMU_TM_NESTED,
+        .s_page_size = DBG_PAGE_SIZE,
+        .g_page_size = DBG_SUPERPAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x24001000ull,
+    }, {
+        .name = "nested-s2m-g4k",
+        .mode = QRIOMMU_TM_NESTED,
+        .s_page_size = DBG_SUPERPAGE_SIZE,
+        .g_page_size = DBG_PAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x24001000ull,
+    }, {
+        .name = "nested-s2m-g2m",
+        .mode = QRIOMMU_TM_NESTED,
+        .s_page_size = DBG_SUPERPAGE_SIZE,
+        .g_page_size = DBG_SUPERPAGE_SIZE,
+        .pa = DBG_TRANSLATED_PA,
+        .expected_response = 0x2403fe00ull,
+    },
+};
+
 static void riscv_config_qpci_bus(QGenericPCIBus *qpci)
 {
     qpci->gpex_pio_base = RISCV_GPEX_PIO_BASE;
@@ -264,6 +328,129 @@ static void test_riscv_iommu_nested(void)
     run_riscv_iommu_translation(&cfg);
 }
 
+static void riscv_iommu_dbg_map_leaf(QTestState *qts, uint64_t l1_pa,
+                                    uint64_t l2_pa, uint64_t va, uint64_t pa,
+                                    uint64_t page_size)
+{
+    uint64_t pte = ((pa >> 12) << 10) | QRIOMMU_LEAF_PTE_RW_MASK;
+    uint64_t pte_addr;
+
+    g_assert_cmphex(pa & (page_size - 1), ==, 0);
+
+    if (page_size == DBG_SUPERPAGE_SIZE) {
+        pte_addr = l1_pa + (((va >> 21) & 0x1ff) * sizeof(uint64_t));
+        qtest_writeq(qts, pte_addr, pte);
+    } else if (page_size == DBG_NAPOT_SIZE) {
+        /* A 64 KiB NAPOT PTE has N=1 and PPN[3:0]=1000. */
+        pte |= BIT_ULL(63) | (8ull << 10);
+        va &= ~(DBG_NAPOT_SIZE - 1);
+        for (unsigned i = 0; i < DBG_NAPOT_SIZE / DBG_PAGE_SIZE; i++) {
+            pte_addr = l2_pa + ((((va >> 12) + i) & 0x1ff) *
+                                sizeof(uint64_t));
+            qtest_writeq(qts, pte_addr, pte);
+        }
+    } else {
+        g_assert_cmphex(page_size, ==, DBG_PAGE_SIZE);
+        pte_addr = l2_pa + (((va >> 12) & 0x1ff) * sizeof(uint64_t));
+        qtest_writeq(qts, pte_addr, pte);
+    }
+}
+
+static uint64_t riscv_iommu_dbg_translate(RiscvIommuTestState *state,
+                                         uint64_t iova)
+{
+    uint64_t request;
+
+    qtest_writeq(state->qts,
+                 state->iommu_base + RISCV_IOMMU_REG_TR_REQ_IOVA, iova);
+    request = ((uint64_t)state->testdev->devfn << 40) |
+              RISCV_IOMMU_TR_REQ_CTL_GO_BUSY;
+    qtest_writeq(state->qts,
+                 state->iommu_base + RISCV_IOMMU_REG_TR_REQ_CTL, request);
+
+    request = qtest_readq(state->qts,
+                          state->iommu_base + RISCV_IOMMU_REG_TR_REQ_CTL);
+    g_assert_cmpuint(request & RISCV_IOMMU_TR_REQ_CTL_GO_BUSY, ==, 0);
+
+    return qtest_readq(state->qts,
+                       state->iommu_base + RISCV_IOMMU_REG_TR_RESPONSE);
+}
+
+static void test_riscv_iommu_dbg(gconstpointer data)
+{
+    const RiscvIommuDbgTest *cfg = data;
+    RiscvIommuTestState state = { 0 };
+    uint64_t iova = QRIOMMU_IOVA & ~(DBG_PAGE_SIZE - 1);
+    uint64_t s_pa = cfg->pa;
+    uint64_t page_size = cfg->mode == QRIOMMU_TM_G_STAGE_ONLY ?
+                         cfg->g_page_size : cfg->s_page_size;
+    uint64_t response;
+
+    if (!riscv_iommu_test_setup(&state)) {
+        return;
+    }
+
+    riscv_iommu_check(state.qts, state.iommu_base, cfg->mode);
+    qriommu_build_translation(state.qts, cfg->mode, state.testdev->devfn);
+
+    if (cfg->mode == QRIOMMU_TM_NESTED) {
+        uint64_t g_l1_pa = QRIOMMU_SPACE_OFFS + QRIOMMU_G_L0_PTE_VAL;
+        uint64_t g_l2_pa = QRIOMMU_SPACE_OFFS + QRIOMMU_G_L1_PTE_VAL;
+        uint64_t offset = iova & (DBG_SUPERPAGE_SIZE - 1);
+        uint64_t gpa = DBG_NESTED_GPA + offset;
+        uint64_t pte_addr;
+        uint64_t pte;
+
+        /* Map all S-stage page tables through one 2 MiB G-stage leaf. */
+        riscv_iommu_dbg_map_leaf(state.qts, g_l1_pa, g_l2_pa, 0,
+                                 QRIOMMU_SPACE_OFFS, DBG_SUPERPAGE_SIZE);
+
+        if (cfg->g_page_size == DBG_PAGE_SIZE) {
+            pte_addr = g_l1_pa + (((gpa >> 21) & 0x1ff) * sizeof(uint64_t));
+            pte = ((g_l2_pa >> 12) << 10) | QRIOMMU_NON_LEAF_PTE_MASK;
+            qtest_writeq(state.qts, pte_addr, pte);
+        }
+        riscv_iommu_dbg_map_leaf(state.qts, g_l1_pa, g_l2_pa, gpa,
+                                 cfg->pa + (offset & ~(cfg->g_page_size - 1)),
+                                 cfg->g_page_size);
+        s_pa = gpa & ~(cfg->s_page_size - 1);
+        page_size = MIN(page_size, cfg->g_page_size);
+    }
+
+    if (cfg->mode == QRIOMMU_TM_G_STAGE_ONLY) {
+        riscv_iommu_dbg_map_leaf(state.qts,
+                                 QRIOMMU_SPACE_OFFS + QRIOMMU_G_L0_PTE_VAL,
+                                 QRIOMMU_SPACE_OFFS + QRIOMMU_G_L1_PTE_VAL,
+                                 iova, cfg->pa, cfg->g_page_size);
+    } else {
+        riscv_iommu_dbg_map_leaf(state.qts,
+                                 QRIOMMU_SPACE_OFFS + QRIOMMU_L0_PTE_VAL,
+                                 QRIOMMU_SPACE_OFFS + QRIOMMU_L1_PTE_VAL,
+                                 iova, s_pa, cfg->s_page_size);
+    }
+    qriommu_program_regs(state.qts, state.iommu_base);
+
+    /*
+     * The spec permits reporting a smaller range. These tests check QEMU's
+     * choice to report the effective range returned by an uncached walk.
+     * No DMA precedes these requests, and debug requests do not fill the IOTLB.
+     */
+    response = riscv_iommu_dbg_translate(&state, iova);
+    g_assert_cmphex(response, ==, cfg->expected_response);
+
+    if (page_size > DBG_PAGE_SIZE) {
+        /* Check both ends as well as the interior of the reported range. */
+        iova &= ~(page_size - 1);
+        response = riscv_iommu_dbg_translate(&state, iova);
+        g_assert_cmphex(response, ==, cfg->expected_response);
+        response = riscv_iommu_dbg_translate(&state,
+                                            iova + page_size - DBG_PAGE_SIZE);
+        g_assert_cmphex(response, ==, cfg->expected_response);
+    }
+
+    riscv_iommu_test_teardown(&state);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -275,5 +462,11 @@ int main(int argc, char **argv)
                    test_riscv_iommu_g_stage_only);
     qtest_add_func("/iommu-testdev/translation/ns-nested",
                    test_riscv_iommu_nested);
+    for (unsigned i = 0; i < G_N_ELEMENTS(debug_tests); i++) {
+        g_autofree char *path = g_strdup_printf("/iommu-testdev/debug/%s",
+                                               debug_tests[i].name);
+
+        qtest_add_data_func(path, &debug_tests[i], test_riscv_iommu_dbg);
+    }
     return g_test_run();
 }
-- 
2.43.0