[PATCH v2 12/20] arm64: Implement try_update_vmemmap_pte using the AF trick
From: James Houghton
Date: Fri Oct 02 2026 - 20:26:01 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_bbm_through_af() 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 | 60 +++++++++++++++++++++++++++++---
1 file changed, 56 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index e47c3d010715..f8e66bb22c0c 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -1249,14 +1249,24 @@ 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)
+/*
+ * Atomically clear the Accessed flag. Return the old value of the PTE.
+ */
+static inline pte_t __ptep_clear_young(pte_t *ptep)
{
atomic64_t *pteval = (atomic64_t *)&pte_val(*ptep);
s64 af_mask = PTE_AF;
- /* Atomically clear PTE_AF, checking that it was set before. */
- return af_mask & atomic64_fetch_andnot_relaxed(af_mask, pteval);
+ /* Atomically clear PTE_AF. */
+ u64 oldval = atomic64_fetch_andnot_relaxed(af_mask, pteval);
+
+ return __pte(oldval);
+}
+
+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,
@@ -1734,6 +1744,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_bbm_through_af())
+ 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.56.0.rc1.315.gc6ed9934b7-goog