[PATCH RFC v10 18/22] arm64: kpkeys: Guard page table writes

From: Kevin Brodsky

Date: Mon Nov 09 2026 - 11:12:06 EST


When CONFIG_KPKEYS_HARDENED_PGTABLES is enabled, page tables (both
user and kernel) are mapped with a privileged pkey in the linear
mapping. As a result, they can only be written in a privileged
kpkeys context.

Introduce a kpkeys guard that sets POR_EL1 appropriately to allow
writing to page tables, and use this guard wherever necessary. The
scope is kept as small as possible, so that POR_EL1 is quickly reset
to its default value. Where atomics are involved, the guard's scope
encompasses the whole loop to avoid switching POR_EL1 unnecessarily.

This patch is a no-op if CONFIG_KPKEYS_HARDENED_PGTABLES is disabled
(default).

Reviewed-by: Yeoreum Yun <yeoreum.yun@xxxxxxx>
Signed-off-by: Kevin Brodsky <kevin.brodsky@xxxxxxx>
---
arch/arm64/include/asm/pgtable.h | 23 ++++++++++++++++++++++-
arch/arm64/mm/fault.c | 17 ++++++++++-------
2 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index a474c9fb485a..5f46d4cb3882 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -39,6 +39,14 @@
#include <linux/mm_types.h>
#include <linux/sched.h>
#include <linux/page_table_check.h>
+#include <linux/kpkeys.h>
+
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+KPKEYS_GUARD_COND(kpkeys_hardened_pgtables, KPKEYS_CTX_PGTABLES,
+ kpkeys_hardened_pgtables_enabled())
+#else
+KPKEYS_GUARD_NOOP(kpkeys_hardened_pgtables)
+#endif

static inline void emit_pte_barriers(void)
{
@@ -360,6 +368,7 @@ static inline pte_t pte_clear_uffd(pte_t pte)

static inline void __set_pte_nosync(pte_t *ptep, pte_t pte)
{
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*ptep, pte);
}

@@ -789,6 +798,7 @@ static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
}
#endif /* __PAGETABLE_PMD_FOLDED */

+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pmdp, pmd);

if (pmd_valid(pmd))
@@ -850,6 +860,7 @@ static inline void set_pud(pud_t *pudp, pud_t pud)
return;
}

+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pudp, pud);

if (pud_valid(pud))
@@ -928,6 +939,7 @@ static inline void set_p4d(p4d_t *p4dp, p4d_t p4d)
return;
}

+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*p4dp, p4d);
queue_pte_barriers();
}
@@ -1053,6 +1065,7 @@ static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
return;
}

+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pgdp, pgd);
queue_pte_barriers();
}
@@ -1254,6 +1267,7 @@ static inline bool __ptep_test_and_clear_young(struct vm_area_struct *vma,
{
pte_t old_pte, pte;

+ guard(kpkeys_hardened_pgtables)();
pte = __ptep_get(ptep);
do {
old_pte = pte;
@@ -1301,7 +1315,10 @@ static inline pte_t __ptep_get_and_clear_anysz(struct mm_struct *mm,
pte_t *ptep,
unsigned long pgsize)
{
- pte_t pte = __pte(xchg_relaxed(&pte_val(*ptep), 0));
+ pte_t pte;
+
+ scoped_guard(kpkeys_hardened_pgtables)
+ pte = __pte(xchg_relaxed(&pte_val(*ptep), 0));

switch (pgsize) {
case PAGE_SIZE:
@@ -1374,6 +1391,7 @@ static inline void ___ptep_set_wrprotect(struct mm_struct *mm,
{
pte_t old_pte;

+ guard(kpkeys_hardened_pgtables)();
do {
old_pte = pte;
pte = pte_wrprotect(pte);
@@ -1407,6 +1425,7 @@ static inline void __clear_young_dirty_pte(struct vm_area_struct *vma,
{
pte_t old_pte;

+ guard(kpkeys_hardened_pgtables)();
do {
old_pte = pte;

@@ -1454,6 +1473,7 @@ static inline pmd_t pmdp_establish(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmdp, pmd_t pmd)
{
page_table_check_pmd_set(vma->vm_mm, address, pmdp, pmd);
+ guard(kpkeys_hardened_pgtables)();
return __pmd(xchg_relaxed(&pmd_val(*pmdp), pmd_val(pmd)));
}
#endif
@@ -1784,6 +1804,7 @@ static inline bool ptep_try_set(pte_t *ptep, pte_t new_pte)
{
pteval_t old = 0;

+ guard(kpkeys_hardened_pgtables)();
if (!try_cmpxchg(&pte_val(*ptep), &old, pte_val(new_pte)))
return false;

diff --git a/arch/arm64/mm/fault.c b/arch/arm64/mm/fault.c
index b22736853fcd..37a4d8187aa1 100644
--- a/arch/arm64/mm/fault.c
+++ b/arch/arm64/mm/fault.c
@@ -239,13 +239,16 @@ int __ptep_set_access_flags_anysz(struct vm_area_struct *vma,
*/
pte_val(entry) ^= PTE_RDONLY;
pteval = pte_val(pte);
- do {
- old_pteval = pteval;
- pteval ^= PTE_RDONLY;
- pteval |= pte_val(entry);
- pteval ^= PTE_RDONLY;
- pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
- } while (pteval != old_pteval);
+ scoped_guard(kpkeys_hardened_pgtables) {
+ do {
+ old_pteval = pteval;
+ pteval ^= PTE_RDONLY;
+ pteval |= pte_val(entry);
+ pteval ^= PTE_RDONLY;
+ pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval,
+ pteval);
+ } while (pteval != old_pteval);
+ }

/*
* Invalidate the local stale read-only entry. Remote stale entries

--
2.56.0