[PATCH 08/18] arm64: Implement try_update_vmemmap_pte using the AF trick

From: James Houghton

Date: Tue Jul 07 2026 - 23:14:44 EST


try_update_vmemmap_pte() must modify vmemmap PTEs without introducing a
time window where other CPUs on the system might fault.

Normally a break-before-make sequence is required to avoid conflicts
with cached translations. However, if we can guarantee that the existing
translation cannot be cached, a BBM sequence is not needed.

Translations with the AF unset may not be cached (see Arm ARM Rule
DWZCQ); the implementation of try_update_vmemmap_pte() on arm64 takes
advantage of this fact to replace a PTE without BBM and therefore
without leaving a window open where PE might fault on this translation.

Of course, if some CPUs on the system do not support HW AF management,
clearing the AF will introduce potential faults. system_supports_hvo()
will return false if any CPUs on the system do not support HW AF.

Signed-off-by: James Houghton <jthoughton@xxxxxxxxxx>
---
arch/arm64/include/asm/pgtable.h | 53 ++++++++++++++++++++++++++++++--
1 file changed, 50 insertions(+), 3 deletions(-)

diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index 5f21d3a738ee..7b11aa41d0a0 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -1302,8 +1302,7 @@ static inline void __pte_clear(struct mm_struct *mm,
__set_pte(ptep, __pte(0));
}

-static inline bool __ptep_test_and_clear_young(struct vm_area_struct *vma,
- unsigned long address, pte_t *ptep)
+static inline pte_t __ptep_clear_young(pte_t *ptep)
{
pte_t old_pte, pte;

@@ -1315,7 +1314,13 @@ static inline bool __ptep_test_and_clear_young(struct vm_area_struct *vma,
pte_val(old_pte), pte_val(pte));
} while (pte_val(pte) != pte_val(old_pte));

- return pte_young(pte);
+ return pte;
+}
+
+static inline bool __ptep_test_and_clear_young(struct vm_area_struct *vma,
+ unsigned long address, pte_t *ptep)
+{
+ return pte_young(__ptep_clear_young(ptep));
}

static inline bool __ptep_clear_flush_young(struct vm_area_struct *vma,
@@ -1793,6 +1798,48 @@ static inline void pte_clear(struct mm_struct *mm,
__pte_clear(mm, addr, ptep);
}

+#define __HAVE_ARCH_TRY_UPDATE_VMEMMAP_PTE
+static inline int try_update_vmemmap_pte(unsigned long addr, pte_t *ptep,
+ const pte_t pte)
+{
+ const int max_attempts = 16;
+ int attempts = 0;
+ pte_t old_pte;
+
+ if (!system_supports_hvo())
+ return -EOPNOTSUPP;
+
+ /* This routine is only to be used for valid-to-valid transitions. */
+ if (WARN_ON_ONCE(!pte_valid(pte)))
+ return -EINVAL;
+
+ old_pte = __ptep_get(ptep);
+
+ do {
+ if (WARN_ON_ONCE(!pte_valid(old_pte)))
+ return -EINVAL;
+
+ /* We should never get a contiguous PTE here. */
+ if (WARN_ON_ONCE(pte_valid_cont(old_pte)))
+ return -EINVAL;
+
+ if (pte_young(old_pte)) {
+ /* __ptep_clear_young() returns the overwritten PTE */
+ old_pte = pte_mkold(__ptep_clear_young(ptep));
+
+ flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
+ }
+ /*
+ * Translations without AF cannot be cached, so we can replace
+ * them without BBM.
+ */
+ } while (!try_cmpxchg_relaxed(&pte_val(*ptep), &pte_val(old_pte),
+ pte_val(pte)) &&
+ ++attempts < max_attempts);
+
+ return attempts == max_attempts ? -EAGAIN : 0;
+}
+
#define clear_full_ptes clear_full_ptes
static inline void clear_full_ptes(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, unsigned int nr, int full)
--
2.55.0.795.g602f6c329a-goog