Skip to contentSkip to conversation

hw/riscv: iommu: Support automatic PTE A/D updates

PATCH In review Tan Chi 4 participants 11 messages Nov 8 – Nov 11 RISC-V

lore
Share & tools
lore

Tan Chi

author
#0 lore raw

cover hw/riscv: iommu: Support automatic PTE A/D updates

This series adds support for automatic updates of page-table accessed
(A) and dirty (D) bits in the RISC-V IOMMU, as specified by the RISC-V IOMMU architecture. The feature is advertised through the AMO_HWAD capability and controlled by the SADE and GADE bits in device contexts.

The first two patches fix implicit G-stage page-table access handling and IOATC permission checks. The third patch implements atomic A/D bit updates using CAS, with page-table walk retries on concurrent modifications and G-stage permission checks for implicit writes during nested translation.

The implementation has been validated with QTests, Linux DMA tests under KVM and TCG, and concurrency tests covering CAS retries, PTE modifications, and G-stage mapping changes.

Tan Chi (3):
hw/riscv: iommu: Fix implicit G-stage page-table accesses hw/riscv: iommu: Check permissions on IOATC hits hw/riscv: iommu: Support automatic PTE A/D updates

 hw/riscv/riscv-iommu.c | 256 +++++++++++++++++++++++++++++++++++++----
 hw/riscv/riscv-iommu.h |   1 +
 2 files changed, 232 insertions(+), 25 deletions(-)
Signature · 1 line
2.53.0
author
#1 lore raw

PATCH 1/3 hw/riscv: iommu: Fix implicit G-stage page-table accesses

When translating a DMA write, G-stage accesses to first-stage page tables are implicit reads. The current implementation checks permissions using the original DMA access type, which may incorrectly require write permission for these reads.

Use read permission for implicit page-table accesses while preserving the original DMA access type for the final translation and fault reporting.

Also track the fault address separately from the translation result and set the implicit-access flag in iotval2 for guest-page faults. Route address-range failures through the common page-fault path to report the correct fault address.

---
 hw/riscv/riscv-iommu.c | 60 ++++++++++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 22 deletions(-)
hw/riscv/riscv-iommu.c +38 -22
index 0e6970773a..2dcc528599 100644
@@ -273,13 +273,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
  * Keeping them separate for now, as error reporting and flow specifics are
  * sufficiently different for separate implementation.
  *
- * @s        : IOMMU Device State
- * @ctx      : Translation context for device id and process address space id.
- * @iotlb    : translation data: physical address and access mode.
- * @return   : success or fault cause code.
+ * @s               : IOMMU Device State
+ * @ctx             : Translation context for device id and process
+ *                    address space id.
+ * @iotlb           : translation data: physical address and access mode.
+ * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
+ * @return          : success or fault cause code.
  */
 static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb)
+    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
 {
     IOMMUAccessFlags trans_perm = IOMMU_NONE;
     dma_addr_t addr, base;
@@ -299,10 +301,10 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
     MemTxResult ret;
     bool pv = !!ctx->process_id;
     /*
-     * Keep the request permission separate from iotlb->perm.  G-stage
+     * Keep the request permission separate from iotlb->perm. G-stage
      * walks translate S-stage PTE addresses before the real leaf is
      * reached, but permission checks and fault types must still use the
-     * original request.  A successful walk leaves iotlb->perm with the
+     * original request. A successful walk leaves iotlb->perm with the
      * effective leaf permission for the translation cache.
      */
     const IOMMUAccessFlags req_perm = iotlb->perm;
@@ -424,6 +426,14 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
         const dma_addr_t pte_addr = base + idx * sc[pass].ptesize;
         const bool ade =
             ctx->tc & (pass ? RISCV_IOMMU_DC_TC_GADE : RISCV_IOMMU_DC_TC_SADE);
+        /*
+         * G-stage accesses to S-stage page tables are implicit reads.
+         * The original DMA permission is used for the final data access.
+         */
+        const bool implicit_read =
+            pass == G_STAGE && en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        const IOMMUAccessFlags acc_perm =
+            implicit_read ? IOMMU_RO : req_perm;
 
         if (ade && !(s->cap & RISCV_IOMMU_CAP_AMO_HWAD)) {
             /* GADE/SADE are reserved bits if AMO_HWAD is cleared.  */
@@ -442,15 +452,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                 masked_msbs = (addr >> (va_len - 1)) & mask;
 
                 if (masked_msbs != 0 && masked_msbs != mask) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_S :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_S;
+                    goto page_fault;
                 }
             } else {
                 if ((addr & va_mask) != addr) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+                    goto page_fault;
                 }
             }
         }
@@ -499,13 +505,13 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
              * non-user mode leaf PTE and !pv we need to fault.
              */
             break;
-        } else if ((req_perm & IOMMU_RO) && !(pte & PTE_R)) {
+        } else if ((acc_perm & IOMMU_RO) && !(pte & PTE_R)) {
             break;                /* Read access check failed */
-        } else if ((req_perm & IOMMU_WO) && !(pte & PTE_W)) {
+        } else if ((acc_perm & IOMMU_WO) && !(pte & PTE_W)) {
             break;                /* Write access check failed */
         } else if (!ade && !(pte & PTE_A)) {
             break;                /* Access bit not set */
-        } else if ((req_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
+        } else if ((acc_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
             break;                /* Dirty bit not set */
         } else if (pass == G_STAGE && !(pte & PTE_U)) {
             /*
@@ -587,13 +593,22 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
         }
     } while (1);
 
+page_fault:
     /*
      * riscv_iommu_translate() will receive a fault and then call
-     * riscv_iommu_report_fault() using iotlb->translated_addr
-     * as iotval2.  Update translated_addr it with the latest
-     * translated addr we have.
+     * riscv_iommu_report_fault() using fault_iotval2 as iotval2.
+     * Update fault_iotval2 with the latest translated address.
+     * For guest-page faults, set the implicit first-stage
+     * page-table access flag in the low bit.
      */
-    iotlb->translated_addr = addr;
+    *fault_iotval2 = addr;
+
+    if (pass == G_STAGE) {
+        bool implicit_read =
+            en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        *fault_iotval2 = (addr & ~3ULL) |
+                         (implicit_read ? 1 : 0);
+    }
 
     return (req_perm & IOMMU_WO) ?
                 (pass ? RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
@@ -1740,6 +1755,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
     bool enable_pri;
     hwaddr phys = 0;
     int fault;
+    uint64_t fault_iotval2 = 0;
 
     riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_URQ);
 
@@ -1776,7 +1792,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
     riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
     /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
 
     if (!fault && iotlb->target_as == &s->trap_as) {
         /* Do not cache trapped MSI translations */
@@ -1827,7 +1843,7 @@ done:
         }
 
         riscv_iommu_report_fault(s, ctx, ttype, fault, enable_pid,
-                                 iotlb->iova, iotlb->translated_addr);
+                                 iotlb->iova, fault_iotval2);
         return fault;
     }
 
Signature · 1 line
2.53.0
#2 lore raw

Re: [PATCH 1/3] hw/riscv: iommu: Fix implicit G-stage page-table accesses

Quoted 114 lines · Tan Chi

When translating a DMA write, G-stage accesses to first-stage page tables are implicit reads. The current implementation checks permissions using the original DMA access type, which may incorrectly require write permission for these reads.

Use read permission for implicit page-table accesses while preserving the original DMA access type for the final translation and fault reporting.

Also track the fault address separately from the translation result and set the implicit-access flag in iotval2 for guest-page faults. Route address-range failures through the common page-fault path to report the correct fault address.

---
  hw/riscv/riscv-iommu.c | 60 ++++++++++++++++++++++++++----------------
  1 file changed, 38 insertions(+), 22 deletions(-)
hw/riscv/riscv-iommu.c +23 -16
index 0e6970773a..2dcc528599 100644
@@ -273,13 +273,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
   * Keeping them separate for now, as error reporting and flow specifics are
   * sufficiently different for separate implementation.
   *
- * @s        : IOMMU Device State
- * @ctx      : Translation context for device id and process address space id.
- * @iotlb    : translation data: physical address and access mode.
- * @return   : success or fault cause code.
+ * @s               : IOMMU Device State
+ * @ctx             : Translation context for device id and process
+ *                    address space id.
+ * @iotlb           : translation data: physical address and access mode.
+ * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
+ * @return          : success or fault cause code.
   */
  static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb)
+    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
  {
      IOMMUAccessFlags trans_perm = IOMMU_NONE;
      dma_addr_t addr, base;
@@ -299,10 +301,10 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      MemTxResult ret;
      bool pv = !!ctx->process_id;
      /*
-     * Keep the request permission separate from iotlb->perm.  G-stage
+     * Keep the request permission separate from iotlb->perm. G-stage
       * walks translate S-stage PTE addresses before the real leaf is
       * reached, but permission checks and fault types must still use the
-     * original request.  A successful walk leaves iotlb->perm with the
+     * original request. A successful walk leaves iotlb->perm with the
       * effective leaf permission for the translation cache.
       */
      const IOMMUAccessFlags req_perm = iotlb->perm;
@@ -424,6 +426,14 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          const dma_addr_t pte_addr = base + idx * sc[pass].ptesize;
          const bool ade =
              ctx->tc & (pass ? RISCV_IOMMU_DC_TC_GADE : RISCV_IOMMU_DC_TC_SADE);
+        /*
+         * G-stage accesses to S-stage page tables are implicit reads.
+         * The original DMA permission is used for the final data access.
+         */
+        const bool implicit_read =
+            pass == G_STAGE && en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        const IOMMUAccessFlags acc_perm =
+            implicit_read ? IOMMU_RO : req_perm;
 
          if (ade && !(s->cap & RISCV_IOMMU_CAP_AMO_HWAD)) {
              /* GADE/SADE are reserved bits if AMO_HWAD is cleared.  */
@@ -442,15 +452,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                  masked_msbs = (addr >> (va_len - 1)) & mask;
 
                  if (masked_msbs != 0 && masked_msbs != mask) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_S :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_S;
+                    goto page_fault;
                  }
              } else {
                  if ((addr & va_mask) != addr) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+                    goto page_fault;
                  }
              }
          }
@@ -499,13 +505,13 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
               * non-user mode leaf PTE and !pv we need to fault.
               */
              break;
-        } else if ((req_perm & IOMMU_RO) && !(pte & PTE_R)) {
+        } else if ((acc_perm & IOMMU_RO) && !(pte & PTE_R)) {
              break;                /* Read access check failed */
-        } else if ((req_perm & IOMMU_WO) && !(pte & PTE_W)) {
+        } else if ((acc_perm & IOMMU_WO) && !(pte & PTE_W)) {
              break;                /* Write access check failed */
          } else if (!ade && !(pte & PTE_A)) {
              break;                /* Access bit not set */
-        } else if ((req_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
+        } else if ((acc_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
              break;                /* Dirty bit not set */
          } else if (pass == G_STAGE && !(pte & PTE_U)) {
              /*
@@ -587,13 +593,22 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          }
      } while (1);
 
+page_fault:
      /*
       * riscv_iommu_translate() will receive a fault and then call

hw/riscv/riscv-iommu.c static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx, · line 604

Quoted 10 lines
hw/riscv/riscv-iommu.c +5 -4
-     * riscv_iommu_report_fault() using iotlb->translated_addr
-     * as iotval2.  Update translated_addr it with the latest
-     * translated addr we have.
+     * riscv_iommu_report_fault() using fault_iotval2 as iotval2.
+     * Update fault_iotval2 with the latest translated address.
+     * For guest-page faults, set the implicit first-stage
+     * page-table access flag in the low bit.
       */
-    iotlb->translated_addr = addr;
+    *fault_iotval2 = addr;

The previous logic was added as a failsafe to not leave iotval2 zeroed during a fault (Gitlab #3559). Now that you're adding a logic to calculate fault_iotval2 based on G_STAGE I think we can change the existing logic to closely match the spec, i.e. iotval2 is supposed to be 0 for S_STAGE faults.

You can set:

*fault_iotval2 = 0;

At this point and then we'll calculate the right value if pass == G_STAGE down below. I tested this change with Gitlab #3559 and it's still a pass. You'll probably want to re-run your own testcases again to be sure but I'm fairly certain this is ok to do.

All the other changes LGTM. Thanks,
Daniel

Quoted 36 lines
hw/riscv/riscv-iommu.c +10 -2
+
+    if (pass == G_STAGE) {
+        bool implicit_read =
+            en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        *fault_iotval2 = (addr & ~3ULL) |
+                         (implicit_read ? 1 : 0);
+    }
 
      return (req_perm & IOMMU_WO) ?
                  (pass ? RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
@@ -1740,6 +1755,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      bool enable_pri;
      hwaddr phys = 0;
      int fault;
+    uint64_t fault_iotval2 = 0;
 
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_URQ);
 
@@ -1776,7 +1792,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
      /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
 
      if (!fault && iotlb->target_as == &s->trap_as) {
          /* Do not cache trapped MSI translations */
@@ -1827,7 +1843,7 @@ done:
          }
 
          riscv_iommu_report_fault(s, ctx, ttype, fault, enable_pid,
-                                 iotlb->iova, iotlb->translated_addr);
+                                 iotlb->iova, fault_iotval2);
          return fault;
      }
 
Reviewed-by on v1 1/3
#3 lore raw

Re: [PATCH 1/3] hw/riscv: iommu: Fix implicit G-stage page-table accesses

In reply to Tan Chi
Quoted 118 lines · Tan Chi

When translating a DMA write, G-stage accesses to first-stage page tables are implicit reads. The current implementation checks permissions using the original DMA access type, which may incorrectly require write permission for these reads.

Use read permission for implicit page-table accesses while preserving the original DMA access type for the final translation and fault reporting.

Also track the fault address separately from the translation result and set the implicit-access flag in iotval2 for guest-page faults. Route address-range failures through the common page-fault path to report the correct fault address.

---
  hw/riscv/riscv-iommu.c | 60 ++++++++++++++++++++++++++----------------
  1 file changed, 38 insertions(+), 22 deletions(-)
hw/riscv/riscv-iommu.c +24 -19
index 0e6970773a..2dcc528599 100644
@@ -273,13 +273,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
   * Keeping them separate for now, as error reporting and flow specifics are
   * sufficiently different for separate implementation.
   *
- * @s        : IOMMU Device State
- * @ctx      : Translation context for device id and process address space id.
- * @iotlb    : translation data: physical address and access mode.
- * @return   : success or fault cause code.
+ * @s               : IOMMU Device State
+ * @ctx             : Translation context for device id and process
+ *                    address space id.
+ * @iotlb           : translation data: physical address and access mode.
+ * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
+ * @return          : success or fault cause code.
   */
  static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb)
+    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
  {
      IOMMUAccessFlags trans_perm = IOMMU_NONE;
      dma_addr_t addr, base;
@@ -299,10 +301,10 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      MemTxResult ret;
      bool pv = !!ctx->process_id;
      /*
-     * Keep the request permission separate from iotlb->perm.  G-stage
+     * Keep the request permission separate from iotlb->perm. G-stage
       * walks translate S-stage PTE addresses before the real leaf is
       * reached, but permission checks and fault types must still use the
-     * original request.  A successful walk leaves iotlb->perm with the
+     * original request. A successful walk leaves iotlb->perm with the
       * effective leaf permission for the translation cache.
       */
      const IOMMUAccessFlags req_perm = iotlb->perm;
@@ -424,6 +426,14 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          const dma_addr_t pte_addr = base + idx * sc[pass].ptesize;
          const bool ade =
              ctx->tc & (pass ? RISCV_IOMMU_DC_TC_GADE : RISCV_IOMMU_DC_TC_SADE);
+        /*
+         * G-stage accesses to S-stage page tables are implicit reads.
+         * The original DMA permission is used for the final data access.
+         */
+        const bool implicit_read =
+            pass == G_STAGE && en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        const IOMMUAccessFlags acc_perm =
+            implicit_read ? IOMMU_RO : req_perm;
 
          if (ade && !(s->cap & RISCV_IOMMU_CAP_AMO_HWAD)) {
              /* GADE/SADE are reserved bits if AMO_HWAD is cleared.  */
@@ -442,15 +452,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                  masked_msbs = (addr >> (va_len - 1)) & mask;
 
                  if (masked_msbs != 0 && masked_msbs != mask) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_S :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_S;
+                    goto page_fault;
                  }
              } else {
                  if ((addr & va_mask) != addr) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+                    goto page_fault;
                  }
              }
          }
@@ -499,13 +505,13 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
               * non-user mode leaf PTE and !pv we need to fault.
               */
              break;
-        } else if ((req_perm & IOMMU_RO) && !(pte & PTE_R)) {
+        } else if ((acc_perm & IOMMU_RO) && !(pte & PTE_R)) {
              break;                /* Read access check failed */
-        } else if ((req_perm & IOMMU_WO) && !(pte & PTE_W)) {
+        } else if ((acc_perm & IOMMU_WO) && !(pte & PTE_W)) {
              break;                /* Write access check failed */
          } else if (!ade && !(pte & PTE_A)) {
              break;                /* Access bit not set */
-        } else if ((req_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
+        } else if ((acc_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
              break;                /* Dirty bit not set */
          } else if (pass == G_STAGE && !(pte & PTE_U)) {
              /*
@@ -587,13 +593,22 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          }
      } while (1);
 
+page_fault:
      /*
       * riscv_iommu_translate() will receive a fault and then call
-     * riscv_iommu_report_fault() using iotlb->translated_addr
-     * as iotval2.  Update translated_addr it with the latest
-     * translated addr we have.
+     * riscv_iommu_report_fault() using fault_iotval2 as iotval2.

hw/riscv/riscv-iommu.c static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx, · line 608

Quoted 10 lines
hw/riscv/riscv-iommu.c +8 -1
+     * Update fault_iotval2 with the latest translated address.
+     * For guest-page faults, set the implicit first-stage
+     * page-table access flag in the low bit.
       */
-    iotlb->translated_addr = addr;
+    *fault_iotval2 = addr;
+
+    if (pass == G_STAGE) {
+        bool implicit_read =
+            en_s && sc[S_STAGE].step != sc[S_STAGE].levels;

One nit pick:
Duplicated implicit_read declaration and computation.

Suggest to declare implicit_read  within function scope. Like:
     const IOMMUAccessFlags req_perm = iotlb->perm;
+    bool implicit_read;
     ...
     do {
           ...
+        implicit_read =
+            pass == G_STAGE && en_s &&
+            sc[S_STAGE].step != sc[S_STAGE].levels;
         ...
     } while (1);

 page_fault:
     *fault_iotval2 = addr;
     if (pass == G_STAGE) {
         *fault_iotval2 = (addr & ~3ULL) | (implicit_read ? 1 : 0); }

Otherwise,

Thanks,
Nutty

Quoted 32 lines
hw/riscv/riscv-iommu.c +6 -2
+        *fault_iotval2 = (addr & ~3ULL) |
+                         (implicit_read ? 1 : 0);
+    }
 
      return (req_perm & IOMMU_WO) ?
                  (pass ? RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
@@ -1740,6 +1755,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      bool enable_pri;
      hwaddr phys = 0;
      int fault;
+    uint64_t fault_iotval2 = 0;
 
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_URQ);
 
@@ -1776,7 +1792,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
      /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
 
      if (!fault && iotlb->target_as == &s->trap_as) {
          /* Do not cache trapped MSI translations */
@@ -1827,7 +1843,7 @@ done:
          }
 
          riscv_iommu_report_fault(s, ctx, ttype, fault, enable_pid,
-                                 iotlb->iova, iotlb->translated_addr);
+                                 iotlb->iova, fault_iotval2);
          return fault;
      }
 
Reviewed-by on v1 1/3
#4 lore raw

Re: [PATCH 1/3] hw/riscv: iommu: Fix implicit G-stage page-table accesses

In reply to Tan Chi
Quoted 118 lines · Tan Chi

When translating a DMA write, G-stage accesses to first-stage page tables are implicit reads. The current implementation checks permissions using the original DMA access type, which may incorrectly require write permission for these reads.

Use read permission for implicit page-table accesses while preserving the original DMA access type for the final translation and fault reporting.

Also track the fault address separately from the translation result and set the implicit-access flag in iotval2 for guest-page faults. Route address-range failures through the common page-fault path to report the correct fault address.

---
  hw/riscv/riscv-iommu.c | 60 ++++++++++++++++++++++++++----------------
  1 file changed, 38 insertions(+), 22 deletions(-)
hw/riscv/riscv-iommu.c +24 -19
index 0e6970773a..2dcc528599 100644
@@ -273,13 +273,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
   * Keeping them separate for now, as error reporting and flow specifics are
   * sufficiently different for separate implementation.
   *
- * @s        : IOMMU Device State
- * @ctx      : Translation context for device id and process address space id.
- * @iotlb    : translation data: physical address and access mode.
- * @return   : success or fault cause code.
+ * @s               : IOMMU Device State
+ * @ctx             : Translation context for device id and process
+ *                    address space id.
+ * @iotlb           : translation data: physical address and access mode.
+ * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
+ * @return          : success or fault cause code.
   */
  static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb)
+    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
  {
      IOMMUAccessFlags trans_perm = IOMMU_NONE;
      dma_addr_t addr, base;
@@ -299,10 +301,10 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      MemTxResult ret;
      bool pv = !!ctx->process_id;
      /*
-     * Keep the request permission separate from iotlb->perm.  G-stage
+     * Keep the request permission separate from iotlb->perm. G-stage
       * walks translate S-stage PTE addresses before the real leaf is
       * reached, but permission checks and fault types must still use the
-     * original request.  A successful walk leaves iotlb->perm with the
+     * original request. A successful walk leaves iotlb->perm with the
       * effective leaf permission for the translation cache.
       */
      const IOMMUAccessFlags req_perm = iotlb->perm;
@@ -424,6 +426,14 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          const dma_addr_t pte_addr = base + idx * sc[pass].ptesize;
          const bool ade =
              ctx->tc & (pass ? RISCV_IOMMU_DC_TC_GADE : RISCV_IOMMU_DC_TC_SADE);
+        /*
+         * G-stage accesses to S-stage page tables are implicit reads.
+         * The original DMA permission is used for the final data access.
+         */
+        const bool implicit_read =
+            pass == G_STAGE && en_s && sc[S_STAGE].step != sc[S_STAGE].levels;
+        const IOMMUAccessFlags acc_perm =
+            implicit_read ? IOMMU_RO : req_perm;
 
          if (ade && !(s->cap & RISCV_IOMMU_CAP_AMO_HWAD)) {
              /* GADE/SADE are reserved bits if AMO_HWAD is cleared.  */
@@ -442,15 +452,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                  masked_msbs = (addr >> (va_len - 1)) & mask;
 
                  if (masked_msbs != 0 && masked_msbs != mask) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_S :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_S;
+                    goto page_fault;
                  }
              } else {
                  if ((addr & va_mask) != addr) {
-                    return (req_perm & IOMMU_WO) ?
-                                RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
-                                RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+                    goto page_fault;
                  }
              }
          }
@@ -499,13 +505,13 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
               * non-user mode leaf PTE and !pv we need to fault.
               */
              break;
-        } else if ((req_perm & IOMMU_RO) && !(pte & PTE_R)) {
+        } else if ((acc_perm & IOMMU_RO) && !(pte & PTE_R)) {
              break;                /* Read access check failed */
-        } else if ((req_perm & IOMMU_WO) && !(pte & PTE_W)) {
+        } else if ((acc_perm & IOMMU_WO) && !(pte & PTE_W)) {
              break;                /* Write access check failed */
          } else if (!ade && !(pte & PTE_A)) {
              break;                /* Access bit not set */
-        } else if ((req_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
+        } else if ((acc_perm & IOMMU_WO) && !ade && !(pte & PTE_D)) {
              break;                /* Dirty bit not set */
          } else if (pass == G_STAGE && !(pte & PTE_U)) {
              /*
@@ -587,13 +593,22 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          }
      } while (1);
 
+page_fault:
      /*
       * riscv_iommu_translate() will receive a fault and then call
-     * riscv_iommu_report_fault() using iotlb->translated_addr
-     * as iotval2.  Update translated_addr it with the latest
-     * translated addr we have.
+     * riscv_iommu_report_fault() using fault_iotval2 as iotval2.

hw/riscv/riscv-iommu.c static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx, · line 608

Quoted 10 lines
hw/riscv/riscv-iommu.c +8 -1
+     * Update fault_iotval2 with the latest translated address.
+     * For guest-page faults, set the implicit first-stage
+     * page-table access flag in the low bit.
       */
-    iotlb->translated_addr = addr;
+    *fault_iotval2 = addr;
+
+    if (pass == G_STAGE) {
+        bool implicit_read =
+            en_s && sc[S_STAGE].step != sc[S_STAGE].levels;

One nit pick:
Duplicated implicit_read declaration and computation.

Suggest to declare implicit_read  within function scope. Like:
     const IOMMUAccessFlags req_perm = iotlb->perm; +    bool implicit_read;
     ...
     do {
           ...
+        implicit_read =
+            pass == G_STAGE && en_s &&
+            sc[S_STAGE].step != sc[S_STAGE].levels;
         ...
     } while (1);

 page_fault:
     *fault_iotval2 = addr;
     if (pass == G_STAGE) {
         *fault_iotval2 = (addr & ~3ULL) | (implicit_read ? 1 : 0); }

Otherwise,

Thanks,
Nutty

Quoted 32 lines
hw/riscv/riscv-iommu.c +6 -2
+        *fault_iotval2 = (addr & ~3ULL) |
+                         (implicit_read ? 1 : 0);
+    }
 
      return (req_perm & IOMMU_WO) ?
                  (pass ? RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
@@ -1740,6 +1755,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      bool enable_pri;
      hwaddr phys = 0;
      int fault;
+    uint64_t fault_iotval2 = 0;
 
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_URQ);
 
@@ -1776,7 +1792,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
      /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
 
      if (!fault && iotlb->target_as == &s->trap_as) {
          /* Do not cache trapped MSI translations */
@@ -1827,7 +1843,7 @@ done:
          }
 
          riscv_iommu_report_fault(s, ctx, ttype, fault, enable_pid,
-                                 iotlb->iova, iotlb->translated_addr);
+                                 iotlb->iova, fault_iotval2);
          return fault;
      }
 
author
#5 lore raw

PATCH 2/3 hw/riscv: iommu: Check permissions on IOATC hits

In reply to Tan Chi

The current implementation reuses cached translations without checking whether their permissions satisfy the requested access.

Only reuse an IOATC entry when its permissions cover the current request. Otherwise, perform a page-table walk to check the requested access. Preserve the existing behavior for requests with IOMMU_NONE.

This is also needed for hardware A/D updates, where mappings with a clear D bit must not cache write permission until the bit has been set.

---
 hw/riscv/riscv-iommu.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
hw/riscv/riscv-iommu.c +3 -1
index 2dcc528599..6760699eb8 100644
@@ -1781,7 +1781,9 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
         phys = iot->phys;
     }
     qemu_mutex_unlock(&s->iot_lock);
-    if (perm != IOMMU_NONE) {
+    if (perm != IOMMU_NONE &&
+        (iotlb->perm == IOMMU_NONE ||
+         (perm & iotlb->perm) == iotlb->perm)) {
         iotlb->translated_addr = PPN_PHYS(phys);
         iotlb->addr_mask = ~TARGET_PAGE_MASK;
         iotlb->perm = perm;
Signature · 1 line
2.53.0
Reviewed-by on v1 2/3
#6 lore raw

Re: [PATCH 2/3] hw/riscv: iommu: Check permissions on IOATC hits

Quoted 7 lines · Tan Chi

The current implementation reuses cached translations without checking whether their permissions satisfy the requested access.

Only reuse an IOATC entry when its permissions cover the current request. Otherwise, perform a page-table walk to check the requested access. Preserve the existing behavior for requests with IOMMU_NONE.

Quoted 5 lines

This is also needed for hardware A/D updates, where mappings with a clear D bit must not cache write permission until the bit has been set.

---

Quoted 18 lines
  hw/riscv/riscv-iommu.c | 4 +++-
  1 file changed, 3 insertions(+), 1 deletion(-)
hw/riscv/riscv-iommu.c +3 -1
index 2dcc528599..6760699eb8 100644
@@ -1781,7 +1781,9 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          phys = iot->phys;
      }
      qemu_mutex_unlock(&s->iot_lock);
-    if (perm != IOMMU_NONE) {
+    if (perm != IOMMU_NONE &&
+        (iotlb->perm == IOMMU_NONE ||
+         (perm & iotlb->perm) == iotlb->perm)) {
          iotlb->translated_addr = PPN_PHYS(phys);
          iotlb->addr_mask = ~TARGET_PAGE_MASK;
          iotlb->perm = perm;
Reviewed-by on v1 2/3
#7 lore raw

Re: [PATCH 2/3] hw/riscv: iommu: Check permissions on IOATC hits

In reply to Tan Chi
Quoted 11 lines · Tan Chi

The current implementation reuses cached translations without checking whether their permissions satisfy the requested access.

Only reuse an IOATC entry when its permissions cover the current request. Otherwise, perform a page-table walk to check the requested access. Preserve the existing behavior for requests with IOMMU_NONE.

This is also needed for hardware A/D updates, where mappings with a clear D bit must not cache write permission until the bit has been set.

Thanks,
Nutty

Quoted 19 lines
---
  hw/riscv/riscv-iommu.c | 4 +++-
  1 file changed, 3 insertions(+), 1 deletion(-)
hw/riscv/riscv-iommu.c +3 -1
index 2dcc528599..6760699eb8 100644
@@ -1781,7 +1781,9 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
          phys = iot->phys;
      }
      qemu_mutex_unlock(&s->iot_lock);
-    if (perm != IOMMU_NONE) {
+    if (perm != IOMMU_NONE &&
+        (iotlb->perm == IOMMU_NONE ||
+         (perm & iotlb->perm) == iotlb->perm)) {
          iotlb->translated_addr = PPN_PHYS(phys);
          iotlb->addr_mask = ~TARGET_PAGE_MASK;
          iotlb->perm = perm;
author
#8 lore raw

PATCH 3/3 hw/riscv: iommu: Support automatic PTE A/D updates

In reply to Tan Chi

Add support for the AMO_HWAD capability to automatically update the accessed and dirty bits of leaf PTEs when SADE or GADE is enabled.

Use atomic compare-and-exchange to update PTEs, restarting the page-table walk if a concurrent modification is detected.

For nested translation, check G-stage write permissions before updating an S-stage PTE. Restart the walk if the write translation resolves to a different physical address.

Avoid caching write permission when the PTE's dirty bit is clear, so subsequent writes can trigger the required update.

Expose the capability through the new "amo-hwad" device property, disabled by default.

---
 hw/riscv/riscv-iommu.c | 196 ++++++++++++++++++++++++++++++++++++++++-
 hw/riscv/riscv-iommu.h |   1 +
 2 files changed, 193 insertions(+), 4 deletions(-)
hw/riscv/riscv-iommu.c +192 -4
index 6760699eb8..04d50cd620 100644
@@ -265,6 +265,128 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
     return page_mask;
 }
 
+typedef enum RISCVIOMMUADResult {
+    RISCV_IOMMU_AD_SUCCESS,
+    RISCV_IOMMU_AD_RETRY,
+    RISCV_IOMMU_AD_FAULT,
+} RISCVIOMMUADResult;
+
+/*
+ * Atomically update the A/D bits of a leaf PTE.
+ * Retry the page-table walk if the PTE has changed.
+ */
+static RISCVIOMMUADResult
+riscv_iommu_ad_update(RISCVIOMMUState *s, hwaddr pte_addr,
+                      unsigned int pte_size, uint64_t pte, uint64_t updated_pte)
+{
+    MemoryRegion *mr;
+    hwaddr offset, len = pte_size;
+    uint64_t old_pte;
+    void *pte_ptr;
+
+    if (pte_addr & (pte_size - 1)) {
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    RCU_READ_LOCK_GUARD();
+
+    mr = address_space_translate(s->target_as, pte_addr, &offset,
+                                 &len, true, MEMTXATTRS_UNSPECIFIED);
+
+    if (len < pte_size || !memory_region_is_ram(mr)) {
+        /*
+         * The entire PTE must be backed by a single RAM region
+         * for an atomic update.
+         */
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    pte_ptr = qemu_map_ram_ptr(mr->ram_block, offset);
+
+    if (pte_size == 4) {
+        uint32_t expected = cpu_to_le32((uint32_t)pte);
+        uint32_t desired = cpu_to_le32((uint32_t)updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint32_t *)pte_ptr, expected, desired);
+        old_pte = le32_to_cpu(old_pte);
+    } else {
+        uint64_t expected = cpu_to_le64(pte);
+        uint64_t desired = cpu_to_le64(updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint64_t *)pte_ptr, expected, desired);
+        old_pte = le64_to_cpu(old_pte);
+    }
+
+    if (old_pte != pte) {
+        return RISCV_IOMMU_AD_RETRY;
+    }
+
+    return RISCV_IOMMU_AD_SUCCESS;
+}
+
+static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2);
+
+/*
+ * Updating a first-stage leaf PTE requires an implicit write.
+ * Translate its GPA through the G-stage, checking write permission
+ * and updating the G-stage's A/D bits as needed.
+ */
+static int riscv_iommu_check_pte_write(RISCVIOMMUState *s,
+                                       RISCVIOMMUContext *ctx,
+                                       dma_addr_t pte_gpa,
+                                       IOMMUAccessFlags req_perm,
+                                       hwaddr *pte_spa,
+                                       uint64_t *fault_iotval2)
+{
+    RISCVIOMMUContext gctx = *ctx;
+    uint64_t iotval2 = 0;
+    int fault;
+
+    IOMMUTLBEntry iotlb = {
+        .iova = pte_gpa,
+        .target_as = s->target_as,
+        .addr_mask = ~0ULL,
+        .perm = IOMMU_WO,
+    };
+
+    gctx.satp = RISCV_IOMMU_DC_FSC_MODE_BARE;
+
+    fault = riscv_iommu_spa_fetch(s, &gctx, &iotlb, true, &iotval2);
+
+    if (fault) {
+        *fault_iotval2 = iotval2;
+
+        if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS ||
+            fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS) {
+            /*
+             * Per the RISC-V IOMMU Fault/Event Queue format, iotval2[0]
+             * indicates an implicit first-stage access and iotval2[1]
+             * indicates an implicit write. Report the fault for the
+             * original DMA access rather than the implicit write.
+             */
+            *fault_iotval2 = (pte_gpa & ~3ULL) | 3;
+
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+        } else if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT ||
+                   fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT) {
+            /*
+             * A physical access fault also uses the original
+             * DMA access type.
+             */
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+        }
+        return fault;
+    }
+
+    *pte_spa = iotlb.translated_addr;
+    return 0;
+}
+
 /*
  * RISCV IOMMU Address Translation Lookup - Page Table Walk
  *
@@ -277,14 +399,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
  * @ctx             : Translation context for device id and process
  *                    address space id.
  * @iotlb           : translation data: physical address and access mode.
+ * @implicit_write  : Whether this is an implicit write to a first-stage PTE.
  * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
  * @return          : success or fault cause code.
  */
 static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2)
 {
     IOMMUAccessFlags trans_perm = IOMMU_NONE;
-    dma_addr_t addr, base;
+    dma_addr_t addr, base, s_base_gpa = 0;
     uint64_t satp, gatp, pte;
     bool en_s, en_g;
     struct {
@@ -308,6 +431,8 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      * effective leaf permission for the translation cache.
      */
     const IOMMUAccessFlags req_perm = iotlb->perm;
+    const hwaddr addr_mask = iotlb->addr_mask;
+    const hwaddr translated_addr = iotlb->translated_addr;
 
     satp = get_field(ctx->satp, RISCV_IOMMU_ATP_MODE_FIELD);
     gatp = get_field(ctx->gatp, RISCV_IOMMU_ATP_MODE_FIELD);
@@ -326,7 +451,7 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      * means we can't do an early MSI check unless we have
      * strictly !en_s.
      */
-    if (!en_s && (req_perm & IOMMU_WO) &&
+    if (!implicit_write && !en_s && (req_perm & IOMMU_WO) &&
         riscv_iommu_msi_check(s, ctx, iotlb->iova)) {
         iotlb->target_as = &s->trap_as;
         iotlb->translated_addr = iotlb->iova;
@@ -413,6 +538,17 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
     /* S/G stages translation tables root pointers */
     gatp = PPN_PHYS(get_field(ctx->gatp, RISCV_IOMMU_ATP_PPN_FIELD));
     satp = PPN_PHYS(get_field(ctx->satp, RISCV_IOMMU_ATP_PPN_FIELD));
+
+restart:
+    sc[S_STAGE].step = 0;
+    sc[G_STAGE].step = 0;
+    trans_perm = IOMMU_NONE;
+    iotlb->addr_mask = addr_mask;
+    iotlb->translated_addr = translated_addr;
+    iotlb->perm = req_perm;
+    s_base_gpa = 0;
+    *fault_iotval2 = 0;
+
     addr = (en_s && en_g) ? satp : iotlb->iova;
     base = en_g ? gatp : satp;
     pass = en_g ? G_STAGE : S_STAGE;
@@ -540,8 +676,51 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                 iotlb->addr_mask &= (1ULL << va_skip) - 1;
             }
 
+            /* Automatically update the leaf PTE's A/D bits when enabled. */
+            uint64_t updated_pte = pte;
+            if (ade) {
+                updated_pte |= PTE_A;
+
+                if (acc_perm & IOMMU_WO) {
+                    updated_pte |= PTE_D;
+                }
+            }
+            if (updated_pte != pte) {
+                if (pass == S_STAGE && en_g) {
+                    const dma_addr_t s_pte_gpa =
+                        s_base_gpa + idx * sc[pass].ptesize;
+                    hwaddr write_spa;
+
+                    int fault = riscv_iommu_check_pte_write(
+                        s, ctx, s_pte_gpa, req_perm, &write_spa,
+                        fault_iotval2);
+                    if (fault) {
+                        return fault;
+                    }
+
+                    if (write_spa != pte_addr) {
+                        goto restart;
+                    }
+                }
+
+                RISCVIOMMUADResult result = riscv_iommu_ad_update(
+                    s, pte_addr, sc[pass].ptesize, pte, updated_pte);
+
+                if (result == RISCV_IOMMU_AD_RETRY) {
+                    goto restart;
+                }
+                if (result == RISCV_IOMMU_AD_FAULT) {
+                    return (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+                }
+                pte = updated_pte;
+            }
+
             /* Continue with S-Stage translation? */
             if (pass && sc[0].step != sc[0].levels) {
+                /* Save the S-stage page-table GPA for A/D updates. */
+                s_base_gpa = addr;
                 pass = S_STAGE;
                 addr = iotlb->iova;
                 continue;
@@ -552,6 +731,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                                          ((pte & PTE_R) ? IOMMU_RW : IOMMU_WO) :
                                          IOMMU_RO;
 
+            /* Do not cache write permission until the D bit is set. */
+            if (!(pte & PTE_D)) {
+                leaf_perm &= ~IOMMU_WO;
+            }
+
             trans_perm = trans_perm == IOMMU_NONE ?
                          leaf_perm : trans_perm & leaf_perm;
 
@@ -1794,7 +1978,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
     riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
     /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, false, &fault_iotval2);
 
     if (!fault && iotlb->target_as == &s->trap_as) {
         /* Do not cache trapped MSI translations */
@@ -2732,6 +2916,9 @@ static void riscv_iommu_realize(DeviceState *dev, Error **errp)
     if (s->enable_ats) {
         s->cap |= RISCV_IOMMU_CAP_ATS;
     }
+    if (s->enable_amo_hwad) {
+        s->cap |= RISCV_IOMMU_CAP_AMO_HWAD;
+    }
     if (s->enable_s_stage) {
         s->cap |= RISCV_IOMMU_CAP_SV32 | RISCV_IOMMU_CAP_SV39 |
                   RISCV_IOMMU_CAP_SV48 | RISCV_IOMMU_CAP_SV57;
@@ -2891,6 +3078,7 @@ static const Property riscv_iommu_properties[] = {
         LIMIT_CACHE_IOT),
     DEFINE_PROP_BOOL("intremap", RISCVIOMMUState, enable_msi, TRUE),
     DEFINE_PROP_BOOL("ats", RISCVIOMMUState, enable_ats, TRUE),
+    DEFINE_PROP_BOOL("amo-hwad", RISCVIOMMUState, enable_amo_hwad, false),
     DEFINE_PROP_BOOL("off", RISCVIOMMUState, enable_off, TRUE),
     DEFINE_PROP_BOOL("s-stage", RISCVIOMMUState, enable_s_stage, TRUE),
     DEFINE_PROP_BOOL("g-stage", RISCVIOMMUState, enable_g_stage, TRUE),
hw/riscv/riscv-iommu.h +1 -0
index 403d10b321..8cf228f698 100644
@@ -44,6 +44,7 @@ struct RISCVIOMMUState {
     bool enable_off;      /* Enable out-of-reset OFF mode (DMA disabled) */
     bool enable_msi;      /* Enable MSI remapping */
     bool enable_ats;      /* Enable ATS support */
+    bool enable_amo_hwad; /* Enable hardware A/D updates */
     bool enable_s_stage;  /* Enable S/VS-Stage translation */
     bool enable_g_stage;  /* Enable G-Stage translation */
 
Signature · 1 line
2.53.0
Reviewed-by on v1 3/3
#9 lore raw
Quoted 10 lines · Tan Chi

Add support for the AMO_HWAD capability to automatically update the accessed and dirty bits of leaf PTEs when SADE or GADE is enabled.

Use atomic compare-and-exchange to update PTEs, restarting the page-table walk if a concurrent modification is detected.

For nested translation, check G-stage write permissions before updating an S-stage PTE. Restart the walk if the write translation resolves to a different physical address.

Quoted 8 lines

Avoid caching write permission when the PTE's dirty bit is clear, so subsequent writes can trigger the required update.

Expose the capability through the new "amo-hwad" device property, disabled by default.

---

Quoted 294 lines
  hw/riscv/riscv-iommu.c | 196 ++++++++++++++++++++++++++++++++++++++++-
  hw/riscv/riscv-iommu.h |   1 +
  2 files changed, 193 insertions(+), 4 deletions(-)
hw/riscv/riscv-iommu.c +192 -4
index 6760699eb8..04d50cd620 100644
@@ -265,6 +265,128 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
      return page_mask;
  }
 
+typedef enum RISCVIOMMUADResult {
+    RISCV_IOMMU_AD_SUCCESS,
+    RISCV_IOMMU_AD_RETRY,
+    RISCV_IOMMU_AD_FAULT,
+} RISCVIOMMUADResult;
+
+/*
+ * Atomically update the A/D bits of a leaf PTE.
+ * Retry the page-table walk if the PTE has changed.
+ */
+static RISCVIOMMUADResult
+riscv_iommu_ad_update(RISCVIOMMUState *s, hwaddr pte_addr,
+                      unsigned int pte_size, uint64_t pte, uint64_t updated_pte)
+{
+    MemoryRegion *mr;
+    hwaddr offset, len = pte_size;
+    uint64_t old_pte;
+    void *pte_ptr;
+
+    if (pte_addr & (pte_size - 1)) {
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    RCU_READ_LOCK_GUARD();
+
+    mr = address_space_translate(s->target_as, pte_addr, &offset,
+                                 &len, true, MEMTXATTRS_UNSPECIFIED);
+
+    if (len < pte_size || !memory_region_is_ram(mr)) {
+        /*
+         * The entire PTE must be backed by a single RAM region
+         * for an atomic update.
+         */
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    pte_ptr = qemu_map_ram_ptr(mr->ram_block, offset);
+
+    if (pte_size == 4) {
+        uint32_t expected = cpu_to_le32((uint32_t)pte);
+        uint32_t desired = cpu_to_le32((uint32_t)updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint32_t *)pte_ptr, expected, desired);
+        old_pte = le32_to_cpu(old_pte);
+    } else {
+        uint64_t expected = cpu_to_le64(pte);
+        uint64_t desired = cpu_to_le64(updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint64_t *)pte_ptr, expected, desired);
+        old_pte = le64_to_cpu(old_pte);
+    }
+
+    if (old_pte != pte) {
+        return RISCV_IOMMU_AD_RETRY;
+    }
+
+    return RISCV_IOMMU_AD_SUCCESS;
+}
+
+static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2);
+
+/*
+ * Updating a first-stage leaf PTE requires an implicit write.
+ * Translate its GPA through the G-stage, checking write permission
+ * and updating the G-stage's A/D bits as needed.
+ */
+static int riscv_iommu_check_pte_write(RISCVIOMMUState *s,
+                                       RISCVIOMMUContext *ctx,
+                                       dma_addr_t pte_gpa,
+                                       IOMMUAccessFlags req_perm,
+                                       hwaddr *pte_spa,
+                                       uint64_t *fault_iotval2)
+{
+    RISCVIOMMUContext gctx = *ctx;
+    uint64_t iotval2 = 0;
+    int fault;
+
+    IOMMUTLBEntry iotlb = {
+        .iova = pte_gpa,
+        .target_as = s->target_as,
+        .addr_mask = ~0ULL,
+        .perm = IOMMU_WO,
+    };
+
+    gctx.satp = RISCV_IOMMU_DC_FSC_MODE_BARE;
+
+    fault = riscv_iommu_spa_fetch(s, &gctx, &iotlb, true, &iotval2);
+
+    if (fault) {
+        *fault_iotval2 = iotval2;
+
+        if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS ||
+            fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS) {
+            /*
+             * Per the RISC-V IOMMU Fault/Event Queue format, iotval2[0]
+             * indicates an implicit first-stage access and iotval2[1]
+             * indicates an implicit write. Report the fault for the
+             * original DMA access rather than the implicit write.
+             */
+            *fault_iotval2 = (pte_gpa & ~3ULL) | 3;
+
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+        } else if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT ||
+                   fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT) {
+            /*
+             * A physical access fault also uses the original
+             * DMA access type.
+             */
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+        }
+        return fault;
+    }
+
+    *pte_spa = iotlb.translated_addr;
+    return 0;
+}
+
  /*
   * RISCV IOMMU Address Translation Lookup - Page Table Walk
   *
@@ -277,14 +399,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
   * @ctx             : Translation context for device id and process
   *                    address space id.
   * @iotlb           : translation data: physical address and access mode.
+ * @implicit_write  : Whether this is an implicit write to a first-stage PTE.
   * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
   * @return          : success or fault cause code.
   */
  static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2)
  {
      IOMMUAccessFlags trans_perm = IOMMU_NONE;
-    dma_addr_t addr, base;
+    dma_addr_t addr, base, s_base_gpa = 0;
      uint64_t satp, gatp, pte;
      bool en_s, en_g;
      struct {
@@ -308,6 +431,8 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
       * effective leaf permission for the translation cache.
       */
      const IOMMUAccessFlags req_perm = iotlb->perm;
+    const hwaddr addr_mask = iotlb->addr_mask;
+    const hwaddr translated_addr = iotlb->translated_addr;
 
      satp = get_field(ctx->satp, RISCV_IOMMU_ATP_MODE_FIELD);
      gatp = get_field(ctx->gatp, RISCV_IOMMU_ATP_MODE_FIELD);
@@ -326,7 +451,7 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
       * means we can't do an early MSI check unless we have
       * strictly !en_s.
       */
-    if (!en_s && (req_perm & IOMMU_WO) &&
+    if (!implicit_write && !en_s && (req_perm & IOMMU_WO) &&
          riscv_iommu_msi_check(s, ctx, iotlb->iova)) {
          iotlb->target_as = &s->trap_as;
          iotlb->translated_addr = iotlb->iova;
@@ -413,6 +538,17 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      /* S/G stages translation tables root pointers */
      gatp = PPN_PHYS(get_field(ctx->gatp, RISCV_IOMMU_ATP_PPN_FIELD));
      satp = PPN_PHYS(get_field(ctx->satp, RISCV_IOMMU_ATP_PPN_FIELD));
+
+restart:
+    sc[S_STAGE].step = 0;
+    sc[G_STAGE].step = 0;
+    trans_perm = IOMMU_NONE;
+    iotlb->addr_mask = addr_mask;
+    iotlb->translated_addr = translated_addr;
+    iotlb->perm = req_perm;
+    s_base_gpa = 0;
+    *fault_iotval2 = 0;
+
      addr = (en_s && en_g) ? satp : iotlb->iova;
      base = en_g ? gatp : satp;
      pass = en_g ? G_STAGE : S_STAGE;
@@ -540,8 +676,51 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                  iotlb->addr_mask &= (1ULL << va_skip) - 1;
              }
 
+            /* Automatically update the leaf PTE's A/D bits when enabled. */
+            uint64_t updated_pte = pte;
+            if (ade) {
+                updated_pte |= PTE_A;
+
+                if (acc_perm & IOMMU_WO) {
+                    updated_pte |= PTE_D;
+                }
+            }
+            if (updated_pte != pte) {
+                if (pass == S_STAGE && en_g) {
+                    const dma_addr_t s_pte_gpa =
+                        s_base_gpa + idx * sc[pass].ptesize;
+                    hwaddr write_spa;
+
+                    int fault = riscv_iommu_check_pte_write(
+                        s, ctx, s_pte_gpa, req_perm, &write_spa,
+                        fault_iotval2);
+                    if (fault) {
+                        return fault;
+                    }
+
+                    if (write_spa != pte_addr) {
+                        goto restart;
+                    }
+                }
+
+                RISCVIOMMUADResult result = riscv_iommu_ad_update(
+                    s, pte_addr, sc[pass].ptesize, pte, updated_pte);
+
+                if (result == RISCV_IOMMU_AD_RETRY) {
+                    goto restart;
+                }
+                if (result == RISCV_IOMMU_AD_FAULT) {
+                    return (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+                }
+                pte = updated_pte;
+            }
+
              /* Continue with S-Stage translation? */
              if (pass && sc[0].step != sc[0].levels) {
+                /* Save the S-stage page-table GPA for A/D updates. */
+                s_base_gpa = addr;
                  pass = S_STAGE;
                  addr = iotlb->iova;
                  continue;
@@ -552,6 +731,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                                           ((pte & PTE_R) ? IOMMU_RW : IOMMU_WO) :
                                           IOMMU_RO;
 
+            /* Do not cache write permission until the D bit is set. */
+            if (!(pte & PTE_D)) {
+                leaf_perm &= ~IOMMU_WO;
+            }
+
              trans_perm = trans_perm == IOMMU_NONE ?
                           leaf_perm : trans_perm & leaf_perm;
 
@@ -1794,7 +1978,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
      /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, false, &fault_iotval2);
 
      if (!fault && iotlb->target_as == &s->trap_as) {
          /* Do not cache trapped MSI translations */
@@ -2732,6 +2916,9 @@ static void riscv_iommu_realize(DeviceState *dev, Error **errp)
      if (s->enable_ats) {
          s->cap |= RISCV_IOMMU_CAP_ATS;
      }
+    if (s->enable_amo_hwad) {
+        s->cap |= RISCV_IOMMU_CAP_AMO_HWAD;
+    }
      if (s->enable_s_stage) {
          s->cap |= RISCV_IOMMU_CAP_SV32 | RISCV_IOMMU_CAP_SV39 |
                    RISCV_IOMMU_CAP_SV48 | RISCV_IOMMU_CAP_SV57;
@@ -2891,6 +3078,7 @@ static const Property riscv_iommu_properties[] = {
          LIMIT_CACHE_IOT),
      DEFINE_PROP_BOOL("intremap", RISCVIOMMUState, enable_msi, TRUE),
      DEFINE_PROP_BOOL("ats", RISCVIOMMUState, enable_ats, TRUE),
+    DEFINE_PROP_BOOL("amo-hwad", RISCVIOMMUState, enable_amo_hwad, false),
      DEFINE_PROP_BOOL("off", RISCVIOMMUState, enable_off, TRUE),
      DEFINE_PROP_BOOL("s-stage", RISCVIOMMUState, enable_s_stage, TRUE),
      DEFINE_PROP_BOOL("g-stage", RISCVIOMMUState, enable_g_stage, TRUE),
hw/riscv/riscv-iommu.h +1 -0
index 403d10b321..8cf228f698 100644
@@ -44,6 +44,7 @@ struct RISCVIOMMUState {
      bool enable_off;      /* Enable out-of-reset OFF mode (DMA disabled) */
      bool enable_msi;      /* Enable MSI remapping */
      bool enable_ats;      /* Enable ATS support */
+    bool enable_amo_hwad; /* Enable hardware A/D updates */
      bool enable_s_stage;  /* Enable S/VS-Stage translation */
      bool enable_g_stage;  /* Enable G-Stage translation */
 
Reviewed-by on v1 3/3
#10 lore raw
In reply to Tan Chi
Quoted 10 lines · Tan Chi

Add support for the AMO_HWAD capability to automatically update the accessed and dirty bits of leaf PTEs when SADE or GADE is enabled.

Use atomic compare-and-exchange to update PTEs, restarting the page-table walk if a concurrent modification is detected.

For nested translation, check G-stage write permissions before updating an S-stage PTE. Restart the walk if the write translation resolves to a different physical address.

Quoted 7 lines

Avoid caching write permission when the PTE's dirty bit is clear, so subsequent writes can trigger the required update.

Expose the capability through the new "amo-hwad" device property, disabled by default.

Thanks,
Nutty

Quoted 295 lines
---
  hw/riscv/riscv-iommu.c | 196 ++++++++++++++++++++++++++++++++++++++++-
  hw/riscv/riscv-iommu.h |   1 +
  2 files changed, 193 insertions(+), 4 deletions(-)
hw/riscv/riscv-iommu.c +192 -4
index 6760699eb8..04d50cd620 100644
@@ -265,6 +265,128 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
      return page_mask;
  }
 
+typedef enum RISCVIOMMUADResult {
+    RISCV_IOMMU_AD_SUCCESS,
+    RISCV_IOMMU_AD_RETRY,
+    RISCV_IOMMU_AD_FAULT,
+} RISCVIOMMUADResult;
+
+/*
+ * Atomically update the A/D bits of a leaf PTE.
+ * Retry the page-table walk if the PTE has changed.
+ */
+static RISCVIOMMUADResult
+riscv_iommu_ad_update(RISCVIOMMUState *s, hwaddr pte_addr,
+                      unsigned int pte_size, uint64_t pte, uint64_t updated_pte)
+{
+    MemoryRegion *mr;
+    hwaddr offset, len = pte_size;
+    uint64_t old_pte;
+    void *pte_ptr;
+
+    if (pte_addr & (pte_size - 1)) {
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    RCU_READ_LOCK_GUARD();
+
+    mr = address_space_translate(s->target_as, pte_addr, &offset,
+                                 &len, true, MEMTXATTRS_UNSPECIFIED);
+
+    if (len < pte_size || !memory_region_is_ram(mr)) {
+        /*
+         * The entire PTE must be backed by a single RAM region
+         * for an atomic update.
+         */
+        return RISCV_IOMMU_AD_FAULT;
+    }
+
+    pte_ptr = qemu_map_ram_ptr(mr->ram_block, offset);
+
+    if (pte_size == 4) {
+        uint32_t expected = cpu_to_le32((uint32_t)pte);
+        uint32_t desired = cpu_to_le32((uint32_t)updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint32_t *)pte_ptr, expected, desired);
+        old_pte = le32_to_cpu(old_pte);
+    } else {
+        uint64_t expected = cpu_to_le64(pte);
+        uint64_t desired = cpu_to_le64(updated_pte);
+
+        old_pte = qatomic_cmpxchg((uint64_t *)pte_ptr, expected, desired);
+        old_pte = le64_to_cpu(old_pte);
+    }
+
+    if (old_pte != pte) {
+        return RISCV_IOMMU_AD_RETRY;
+    }
+
+    return RISCV_IOMMU_AD_SUCCESS;
+}
+
+static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2);
+
+/*
+ * Updating a first-stage leaf PTE requires an implicit write.
+ * Translate its GPA through the G-stage, checking write permission
+ * and updating the G-stage's A/D bits as needed.
+ */
+static int riscv_iommu_check_pte_write(RISCVIOMMUState *s,
+                                       RISCVIOMMUContext *ctx,
+                                       dma_addr_t pte_gpa,
+                                       IOMMUAccessFlags req_perm,
+                                       hwaddr *pte_spa,
+                                       uint64_t *fault_iotval2)
+{
+    RISCVIOMMUContext gctx = *ctx;
+    uint64_t iotval2 = 0;
+    int fault;
+
+    IOMMUTLBEntry iotlb = {
+        .iova = pte_gpa,
+        .target_as = s->target_as,
+        .addr_mask = ~0ULL,
+        .perm = IOMMU_WO,
+    };
+
+    gctx.satp = RISCV_IOMMU_DC_FSC_MODE_BARE;
+
+    fault = riscv_iommu_spa_fetch(s, &gctx, &iotlb, true, &iotval2);
+
+    if (fault) {
+        *fault_iotval2 = iotval2;
+
+        if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS ||
+            fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS) {
+            /*
+             * Per the RISC-V IOMMU Fault/Event Queue format, iotval2[0]
+             * indicates an implicit first-stage access and iotval2[1]
+             * indicates an implicit write. Report the fault for the
+             * original DMA access rather than the implicit write.
+             */
+            *fault_iotval2 = (pte_gpa & ~3ULL) | 3;
+
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT_VS :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT_VS;
+        } else if (fault == RISCV_IOMMU_FQ_CAUSE_RD_FAULT ||
+                   fault == RISCV_IOMMU_FQ_CAUSE_WR_FAULT) {
+            /*
+             * A physical access fault also uses the original
+             * DMA access type.
+             */
+            fault = (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+        }
+        return fault;
+    }
+
+    *pte_spa = iotlb.translated_addr;
+    return 0;
+}
+
  /*
   * RISCV IOMMU Address Translation Lookup - Page Table Walk
   *
@@ -277,14 +399,15 @@ static hwaddr riscv_iommu_napot_page_mask(hwaddr ppn, hwaddr addr, hwaddr *out)
   * @ctx             : Translation context for device id and process
   *                    address space id.
   * @iotlb           : translation data: physical address and access mode.
+ * @implicit_write  : Whether this is an implicit write to a first-stage PTE.
   * @fault_iotval2   : Fault address and implicit-access flags for iotval2.
   * @return          : success or fault cause code.
   */
  static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
-    IOMMUTLBEntry *iotlb, uint64_t *fault_iotval2)
+    IOMMUTLBEntry *iotlb, bool implicit_write, uint64_t *fault_iotval2)
  {
      IOMMUAccessFlags trans_perm = IOMMU_NONE;
-    dma_addr_t addr, base;
+    dma_addr_t addr, base, s_base_gpa = 0;
      uint64_t satp, gatp, pte;
      bool en_s, en_g;
      struct {
@@ -308,6 +431,8 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
       * effective leaf permission for the translation cache.
       */
      const IOMMUAccessFlags req_perm = iotlb->perm;
+    const hwaddr addr_mask = iotlb->addr_mask;
+    const hwaddr translated_addr = iotlb->translated_addr;
 
      satp = get_field(ctx->satp, RISCV_IOMMU_ATP_MODE_FIELD);
      gatp = get_field(ctx->gatp, RISCV_IOMMU_ATP_MODE_FIELD);
@@ -326,7 +451,7 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
       * means we can't do an early MSI check unless we have
       * strictly !en_s.
       */
-    if (!en_s && (req_perm & IOMMU_WO) &&
+    if (!implicit_write && !en_s && (req_perm & IOMMU_WO) &&
          riscv_iommu_msi_check(s, ctx, iotlb->iova)) {
          iotlb->target_as = &s->trap_as;
          iotlb->translated_addr = iotlb->iova;
@@ -413,6 +538,17 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      /* S/G stages translation tables root pointers */
      gatp = PPN_PHYS(get_field(ctx->gatp, RISCV_IOMMU_ATP_PPN_FIELD));
      satp = PPN_PHYS(get_field(ctx->satp, RISCV_IOMMU_ATP_PPN_FIELD));
+
+restart:
+    sc[S_STAGE].step = 0;
+    sc[G_STAGE].step = 0;
+    trans_perm = IOMMU_NONE;
+    iotlb->addr_mask = addr_mask;
+    iotlb->translated_addr = translated_addr;
+    iotlb->perm = req_perm;
+    s_base_gpa = 0;
+    *fault_iotval2 = 0;
+
      addr = (en_s && en_g) ? satp : iotlb->iova;
      base = en_g ? gatp : satp;
      pass = en_g ? G_STAGE : S_STAGE;
@@ -540,8 +676,51 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                  iotlb->addr_mask &= (1ULL << va_skip) - 1;
              }
 
+            /* Automatically update the leaf PTE's A/D bits when enabled. */
+            uint64_t updated_pte = pte;
+            if (ade) {
+                updated_pte |= PTE_A;
+
+                if (acc_perm & IOMMU_WO) {
+                    updated_pte |= PTE_D;
+                }
+            }
+            if (updated_pte != pte) {
+                if (pass == S_STAGE && en_g) {
+                    const dma_addr_t s_pte_gpa =
+                        s_base_gpa + idx * sc[pass].ptesize;
+                    hwaddr write_spa;
+
+                    int fault = riscv_iommu_check_pte_write(
+                        s, ctx, s_pte_gpa, req_perm, &write_spa,
+                        fault_iotval2);
+                    if (fault) {
+                        return fault;
+                    }
+
+                    if (write_spa != pte_addr) {
+                        goto restart;
+                    }
+                }
+
+                RISCVIOMMUADResult result = riscv_iommu_ad_update(
+                    s, pte_addr, sc[pass].ptesize, pte, updated_pte);
+
+                if (result == RISCV_IOMMU_AD_RETRY) {
+                    goto restart;
+                }
+                if (result == RISCV_IOMMU_AD_FAULT) {
+                    return (req_perm & IOMMU_WO) ?
+                        RISCV_IOMMU_FQ_CAUSE_WR_FAULT :
+                        RISCV_IOMMU_FQ_CAUSE_RD_FAULT;
+                }
+                pte = updated_pte;
+            }
+
              /* Continue with S-Stage translation? */
              if (pass && sc[0].step != sc[0].levels) {
+                /* Save the S-stage page-table GPA for A/D updates. */
+                s_base_gpa = addr;
                  pass = S_STAGE;
                  addr = iotlb->iova;
                  continue;
@@ -552,6 +731,11 @@ static int riscv_iommu_spa_fetch(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
                                           ((pte & PTE_R) ? IOMMU_RW : IOMMU_WO) :
                                           IOMMU_RO;
 
+            /* Do not cache write permission until the D bit is set. */
+            if (!(pte & PTE_D)) {
+                leaf_perm &= ~IOMMU_WO;
+            }
+
              trans_perm = trans_perm == IOMMU_NONE ?
                           leaf_perm : trans_perm & leaf_perm;
 
@@ -1794,7 +1978,7 @@ static int riscv_iommu_translate(RISCVIOMMUState *s, RISCVIOMMUContext *ctx,
      riscv_iommu_hpm_incr_ctr(s, ctx, RISCV_IOMMU_HPMEVENT_TLB_MISS);
 
      /* Translate using device directory / page table information. */
-    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, &fault_iotval2);
+    fault = riscv_iommu_spa_fetch(s, ctx, iotlb, false, &fault_iotval2);
 
      if (!fault && iotlb->target_as == &s->trap_as) {
          /* Do not cache trapped MSI translations */
@@ -2732,6 +2916,9 @@ static void riscv_iommu_realize(DeviceState *dev, Error **errp)
      if (s->enable_ats) {
          s->cap |= RISCV_IOMMU_CAP_ATS;
      }
+    if (s->enable_amo_hwad) {
+        s->cap |= RISCV_IOMMU_CAP_AMO_HWAD;
+    }
      if (s->enable_s_stage) {
          s->cap |= RISCV_IOMMU_CAP_SV32 | RISCV_IOMMU_CAP_SV39 |
                    RISCV_IOMMU_CAP_SV48 | RISCV_IOMMU_CAP_SV57;
@@ -2891,6 +3078,7 @@ static const Property riscv_iommu_properties[] = {
          LIMIT_CACHE_IOT),
      DEFINE_PROP_BOOL("intremap", RISCVIOMMUState, enable_msi, TRUE),
      DEFINE_PROP_BOOL("ats", RISCVIOMMUState, enable_ats, TRUE),
+    DEFINE_PROP_BOOL("amo-hwad", RISCVIOMMUState, enable_amo_hwad, false),
      DEFINE_PROP_BOOL("off", RISCVIOMMUState, enable_off, TRUE),
      DEFINE_PROP_BOOL("s-stage", RISCVIOMMUState, enable_s_stage, TRUE),
      DEFINE_PROP_BOOL("g-stage", RISCVIOMMUState, enable_g_stage, TRUE),
hw/riscv/riscv-iommu.h +1 -0
index 403d10b321..8cf228f698 100644
@@ -44,6 +44,7 @@ struct RISCVIOMMUState {
      bool enable_off;      /* Enable out-of-reset OFF mode (DMA disabled) */
      bool enable_msi;      /* Enable MSI remapping */
      bool enable_ats;      /* Enable ATS support */
+    bool enable_amo_hwad; /* Enable hardware A/D updates */
      bool enable_s_stage;  /* Enable S/VS-Stage translation */
      bool enable_g_stage;  /* Enable G-Stage translation */