* [PATCH RFC v7 01/24] mm: Introduce kpkeys
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 02/24] set_memory: Introduce set_memory_pkey() stub Kevin Brodsky
` (22 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
kpkeys is a simple framework to enable the use of protection keys
(pkeys) to harden the kernel itself. This patch introduces the basic
API in <linux/kpkeys.h>: a couple of functions to set and restore
the pkey register and macros to define guard objects.
kpkeys introduces a new concept on top of pkeys: the kpkeys context.
Each context is associated to a set of permissions for the pkeys
managed by the kpkeys framework. kpkeys_set_context(ctx) sets those
permissions according to ctx, and returns the original pkey
register, to be later restored by kpkeys_restore_pkey_reg(). To
start with, only KPKEYS_CTX_DEFAULT is available, which is meant to
grant RW access to KPKEYS_PKEY_DEFAULT (i.e. all memory since this
is the only available pkey for now).
Because each architecture implementing pkeys uses a different
representation for the pkey register, and may reserve certain pkeys
for specific uses, support for kpkeys must be explicitly indicated
by selecting ARCH_HAS_KPKEYS and defining the following functions in
<asm/kpkeys.h>, in addition to the macros provided in
<asm-generic/kpkeys.h>:
- arch_kpkeys_set_context()
- arch_kpkeys_restore_pkey_reg()
- arch_supports_kpkeys()
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/asm-generic/kpkeys.h | 17 +++++++
include/linux/kpkeys.h | 118 +++++++++++++++++++++++++++++++++++++++++++
mm/Kconfig | 2 +
3 files changed, 137 insertions(+)
diff --git a/include/asm-generic/kpkeys.h b/include/asm-generic/kpkeys.h
new file mode 100644
index 000000000000..ab819f157d6a
--- /dev/null
+++ b/include/asm-generic/kpkeys.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_GENERIC_KPKEYS_H
+#define __ASM_GENERIC_KPKEYS_H
+
+#ifndef KPKEYS_PKEY_DEFAULT
+#define KPKEYS_PKEY_DEFAULT 0
+#endif
+
+/*
+ * Represents a pkey register value that cannot be used, typically disabling
+ * access to all keys.
+ */
+#ifndef KPKEYS_PKEY_REG_INVAL
+#define KPKEYS_PKEY_REG_INVAL 0
+#endif
+
+#endif /* __ASM_GENERIC_KPKEYS_H */
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
new file mode 100644
index 000000000000..cb2d22758391
--- /dev/null
+++ b/include/linux/kpkeys.h
@@ -0,0 +1,118 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_KPKEYS_H
+#define _LINUX_KPKEYS_H
+
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+
+#define KPKEYS_CTX_DEFAULT 0
+
+#define KPKEYS_CTX_MIN KPKEYS_CTX_DEFAULT
+#define KPKEYS_CTX_MAX KPKEYS_CTX_DEFAULT
+
+#define __KPKEYS_GUARD(name, set_context, restore_pkey_reg, set_arg, ...) \
+ __DEFINE_CLASS_IS_CONDITIONAL(name, false); \
+ DEFINE_CLASS(name, u64, \
+ restore_pkey_reg, set_context, set_arg); \
+ static inline void *class_##name##_lock_ptr(u64 *_T) \
+ { return _T; }
+
+/**
+ * KPKEYS_GUARD_NOOP() - define a guard type that does nothing
+ * @name: the name of the guard type
+ * @cond_arg: an argument specification (optional)
+ *
+ * Define a guard type that does nothing, useful to match a real guard type
+ * that is defined under an #ifdef. @cond_arg may optionally be passed to match
+ * a guard defined using KPKEYS_GUARD_COND().
+ */
+#define KPKEYS_GUARD_NOOP(name, ...) \
+ __KPKEYS_GUARD(name, 0, (void)_T, ##__VA_ARGS__, void)
+
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+
+#include <asm/kpkeys.h>
+
+/**
+ * KPKEYS_GUARD_COND() - define a guard type that conditionally switches to
+ * a given kpkeys context
+ * @name: the name of the guard type
+ * @ctx: the kpkeys context to switch to
+ * @cond: an expression that is evaluated as condition
+ * @cond_arg: an argument specification for the condition (optional)
+ *
+ * Define a guard type that switches to @ctx if @cond evaluates to true,
+ * and does nothing otherwise. @cond_arg may be specified to give access to a
+ * caller-defined argument to @cond.
+ */
+#define KPKEYS_GUARD_COND(name, ctx, cond, ...) \
+ __KPKEYS_GUARD(name, \
+ cond ? kpkeys_set_context(ctx) \
+ : KPKEYS_PKEY_REG_INVAL, \
+ kpkeys_restore_pkey_reg(_T), \
+ ##__VA_ARGS__, void)
+
+/**
+ * KPKEYS_GUARD() - define a guard type that switches to a given kpkeys context
+ * if kpkeys are enabled
+ * @name: the name of the guard type
+ * @ctx: the kpkeys context to switch to
+ *
+ * Define a guard type that switches to @ctx if the system supports kpkeys.
+ */
+#define KPKEYS_GUARD(name, ctx) \
+ KPKEYS_GUARD_COND(name, ctx, kpkeys_enabled())
+
+/**
+ * kpkeys_set_context() - switch kpkeys context
+ * @ctx: the context to switch to
+ *
+ * Switches to specified kpkeys context. @ctx must be a compile-time
+ * constant. The arch-specific pkey register will be updated accordingly, and
+ * the original value returned.
+ *
+ * Return: the original pkey register value if the register was written to, or
+ * KPKEYS_PKEY_REG_INVAL otherwise (no write to the register was
+ * required).
+ */
+static __always_inline u64 kpkeys_set_context(int ctx)
+{
+ BUILD_BUG_ON_MSG(!__builtin_constant_p(ctx),
+ "kpkeys_set_context() only takes constant values");
+ BUILD_BUG_ON_MSG(ctx < KPKEYS_CTX_MIN || ctx > KPKEYS_CTX_MAX,
+ "Invalid value passed to kpkeys_set_context()");
+
+ return arch_kpkeys_set_context(ctx);
+}
+
+/**
+ * kpkeys_restore_pkey_reg() - restores a pkey register value
+ * @pkey_reg: the pkey register value to restore
+ *
+ * This function is meant to be passed the value returned by
+ * kpkeys_set_context(), in order to restore the pkey register to its original
+ * value (thus restoring the original kpkeys context).
+ */
+static __always_inline void kpkeys_restore_pkey_reg(u64 pkey_reg)
+{
+ if (pkey_reg != KPKEYS_PKEY_REG_INVAL)
+ arch_kpkeys_restore_pkey_reg(pkey_reg);
+}
+
+static inline bool kpkeys_enabled(void)
+{
+ return arch_supports_kpkeys();
+}
+
+#else /* CONFIG_ARCH_HAS_KPKEYS */
+
+#include <asm-generic/kpkeys.h>
+
+static inline bool kpkeys_enabled(void)
+{
+ return false;
+}
+
+#endif /* CONFIG_ARCH_HAS_KPKEYS */
+
+#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/Kconfig b/mm/Kconfig
index e8bf1e9e6ad9..819fb0d7b7bd 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1242,6 +1242,8 @@ config ARCH_USES_HIGH_VMA_FLAGS
bool
config ARCH_HAS_PKEYS
bool
+config ARCH_HAS_KPKEYS
+ bool
config ARCH_USES_PG_ARCH_2
bool
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 02/24] set_memory: Introduce set_memory_pkey() stub
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 01/24] mm: Introduce kpkeys Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 03/24] arm64: mm: Enable overlays for all EL1 indirect permissions Kevin Brodsky
` (21 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Introduce a new function, set_memory_pkey(), which sets the
protection key (pkey) of pages in the specified linear mapping
range. Architectures implementing kernel pkeys (kpkeys) must
provide a suitable implementation; an empty stub is added as
fallback.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/set_memory.h | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h
index 3030d9245f5a..7b3a8bfde3c6 100644
--- a/include/linux/set_memory.h
+++ b/include/linux/set_memory.h
@@ -84,4 +84,11 @@ static inline int set_memory_decrypted(unsigned long addr, int numpages)
}
#endif /* CONFIG_ARCH_HAS_MEM_ENCRYPT */
+#ifndef CONFIG_ARCH_HAS_KPKEYS
+static inline int set_memory_pkey(unsigned long addr, int numpages, int pkey)
+{
+ return 0;
+}
+#endif
+
#endif /* _LINUX_SET_MEMORY_H_ */
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 03/24] arm64: mm: Enable overlays for all EL1 indirect permissions
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 01/24] mm: Introduce kpkeys Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 02/24] set_memory: Introduce set_memory_pkey() stub Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 04/24] arm64: Introduce por_elx_set_pkey_perms() helper Kevin Brodsky
` (20 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
In preparation of using POE inside the kernel, enable "Overlay
applied" for all stage 1 base permissions in PIR_EL1. This ensures
that the permissions set in POR_EL1 affect all kernel mappings.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable-prot.h | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable-prot.h b/arch/arm64/include/asm/pgtable-prot.h
index 212ce1b02e15..3a05a80e7959 100644
--- a/arch/arm64/include/asm/pgtable-prot.h
+++ b/arch/arm64/include/asm/pgtable-prot.h
@@ -179,13 +179,13 @@ static inline bool __pure lpa2_is_enabled(void)
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_GCS), PIE_NONE_O) | \
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_GCS_RO), PIE_NONE_O) | \
PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_EXECONLY), PIE_NONE_O) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_READONLY_EXEC), PIE_R) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED_EXEC), PIE_RW) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_READONLY), PIE_R) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED), PIE_RW) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_ROX), PIE_RX) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_EXEC), PIE_RWX) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_RO), PIE_R) | \
- PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL), PIE_RW))
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_READONLY_EXEC), PIE_R_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED_EXEC), PIE_RW_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_READONLY), PIE_R_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_SHARED), PIE_RW_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_ROX), PIE_RX_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_EXEC), PIE_RWX_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL_RO), PIE_R_O) | \
+ PIRx_ELx_PERM_PREP(pte_pi_index(_PAGE_KERNEL), PIE_RW_O))
#endif /* __ASM_PGTABLE_PROT_H */
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 04/24] arm64: Introduce por_elx_set_pkey_perms() helper
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (2 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 03/24] arm64: mm: Enable overlays for all EL1 indirect permissions Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 05/24] arm64: Implement asm/kpkeys.h using POE Kevin Brodsky
` (19 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Introduce a helper that sets the permissions of a given pkey
(POIndex) in the POR_ELx format, and make use of it in
arch_set_user_pkey_access().
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/por.h | 7 +++++++
arch/arm64/mm/mmu.c | 26 ++++++++++----------------
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/arch/arm64/include/asm/por.h b/arch/arm64/include/asm/por.h
index d913d5b529e4..bffb4d2b1246 100644
--- a/arch/arm64/include/asm/por.h
+++ b/arch/arm64/include/asm/por.h
@@ -31,4 +31,11 @@ static inline bool por_elx_allows_exec(u64 por, u8 pkey)
return perm & POE_X;
}
+static inline u64 por_elx_set_pkey_perms(u64 por, u8 pkey, u64 perms)
+{
+ u64 shift = POR_ELx_PERM_SHIFT(pkey);
+
+ return (por & ~(POE_MASK << shift)) | (perms << shift);
+}
+
#endif /* _ASM_ARM64_POR_H */
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index dd85e093ffdb..493310cf0486 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -2339,8 +2339,8 @@ void __cpu_replace_ttbr1(pgd_t *pgdp, bool cnp)
#ifdef CONFIG_ARCH_HAS_PKEYS
int arch_set_user_pkey_access(int pkey, unsigned long init_val)
{
- u64 new_por;
- u64 old_por;
+ u64 new_perms;
+ u64 por;
if (!system_supports_poe())
return -ENOSPC;
@@ -2354,25 +2354,19 @@ int arch_set_user_pkey_access(int pkey, unsigned long init_val)
return -EINVAL;
/* Set the bits we need in POR: */
- new_por = POE_RWX;
+ new_perms = POE_RWX;
if (init_val & PKEY_DISABLE_WRITE)
- new_por &= ~POE_W;
+ new_perms &= ~POE_W;
if (init_val & PKEY_DISABLE_ACCESS)
- new_por &= ~POE_RW;
+ new_perms &= ~POE_RW;
if (init_val & PKEY_DISABLE_READ)
- new_por &= ~POE_R;
+ new_perms &= ~POE_R;
if (init_val & PKEY_DISABLE_EXECUTE)
- new_por &= ~POE_X;
+ new_perms &= ~POE_X;
- /* Shift the bits in to the correct place in POR for pkey: */
- new_por = POR_ELx_PERM_PREP(pkey, new_por);
-
- /* Get old POR and mask off any old bits in place: */
- old_por = read_sysreg_s(SYS_POR_EL0);
- old_por &= ~(POE_MASK << POR_ELx_PERM_SHIFT(pkey));
-
- /* Write old part along with new part: */
- write_sysreg_s(old_por | new_por, SYS_POR_EL0);
+ por = read_sysreg_s(SYS_POR_EL0);
+ por = por_elx_set_pkey_perms(por, pkey, new_perms);
+ write_sysreg_s(por, SYS_POR_EL0);
return 0;
}
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 05/24] arm64: Implement asm/kpkeys.h using POE
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (3 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 04/24] arm64: Introduce por_elx_set_pkey_perms() helper Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 06/24] arm64: set_memory: Implement set_memory_pkey() Kevin Brodsky
` (18 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Implement the kpkeys interface if CONFIG_ARM64_POE is enabled.
The permissions for KPKEYS_PKEY_DEFAULT (pkey 0) are set to RWX as
this pkey is also used for code mappings.
To allow <asm/kpkeys.h> to be included from assembly, also add
appropriate #ifdef's to <asm/por.h>.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 59 +++++++++++++++++++++++++++++++++++++++++
arch/arm64/include/asm/por.h | 4 +++
2 files changed, 63 insertions(+)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
new file mode 100644
index 000000000000..7dad3532cacf
--- /dev/null
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_KPKEYS_H
+#define __ASM_KPKEYS_H
+
+#include <asm/barrier.h>
+#include <asm/cpufeature.h>
+#include <asm/por.h>
+
+#include <asm-generic/kpkeys.h>
+
+/*
+ * Equivalent to por_set_kpkeys_context(0, KPKEYS_CTX_DEFAULT), but can also be
+ * used in assembly.
+ */
+#define POR_EL1_INIT POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX)
+
+#ifndef __ASSEMBLY__
+
+static inline bool arch_supports_kpkeys(void)
+{
+ return system_supports_poe();
+}
+
+#ifdef CONFIG_ARM64_POE
+
+static inline u64 por_set_kpkeys_context(u64 por, int ctx)
+{
+ por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_DEFAULT, POE_RWX);
+
+ return por;
+}
+
+static __always_inline void __kpkeys_set_pkey_reg_nosync(u64 pkey_reg)
+{
+ write_sysreg_s(pkey_reg, SYS_POR_EL1);
+}
+
+static __always_inline int arch_kpkeys_set_context(int ctx)
+{
+ u64 prev_por = read_sysreg_s(SYS_POR_EL1);
+ u64 new_por = por_set_kpkeys_context(prev_por, ctx);
+
+ __kpkeys_set_pkey_reg_nosync(new_por);
+ isb();
+
+ return prev_por;
+}
+
+static __always_inline void arch_kpkeys_restore_pkey_reg(u64 pkey_reg)
+{
+ __kpkeys_set_pkey_reg_nosync(pkey_reg);
+ isb();
+}
+
+#endif /* CONFIG_ARM64_POE */
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* __ASM_KPKEYS_H */
diff --git a/arch/arm64/include/asm/por.h b/arch/arm64/include/asm/por.h
index bffb4d2b1246..58dce4b8021b 100644
--- a/arch/arm64/include/asm/por.h
+++ b/arch/arm64/include/asm/por.h
@@ -10,6 +10,8 @@
#define POR_EL0_INIT POR_ELx_PERM_PREP(0, POE_RWX)
+#ifndef __ASSEMBLY__
+
static inline bool por_elx_allows_read(u64 por, u8 pkey)
{
u8 perm = POR_ELx_PERM_GET(pkey, por);
@@ -38,4 +40,6 @@ static inline u64 por_elx_set_pkey_perms(u64 por, u8 pkey, u64 perms)
return (por & ~(POE_MASK << shift)) | (perms << shift);
}
+#endif /* __ASSEMBLY__ */
+
#endif /* _ASM_ARM64_POR_H */
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 06/24] arm64: set_memory: Implement set_memory_pkey()
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (4 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 05/24] arm64: Implement asm/kpkeys.h using POE Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 07/24] arm64: Context-switch POR_EL1 Kevin Brodsky
` (17 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Implement set_memory_pkey() using POE.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/set_memory.h | 4 ++++
arch/arm64/mm/pageattr.c | 26 ++++++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/arch/arm64/include/asm/set_memory.h b/arch/arm64/include/asm/set_memory.h
index 90f61b17275e..b6cd6de34abf 100644
--- a/arch/arm64/include/asm/set_memory.h
+++ b/arch/arm64/include/asm/set_memory.h
@@ -19,4 +19,8 @@ bool kernel_page_present(struct page *page);
int set_memory_encrypted(unsigned long addr, int numpages);
int set_memory_decrypted(unsigned long addr, int numpages);
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+int set_memory_pkey(unsigned long addr, int numpages, int pkey);
+#endif
+
#endif /* _ASM_ARM64_SET_MEMORY_H */
diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index ce035e1b4eaf..ecdfaa9701e1 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -9,6 +9,8 @@
#include <linux/sched.h>
#include <linux/vmalloc.h>
#include <linux/pagewalk.h>
+#include <linux/pkeys.h>
+#include <linux/kpkeys.h>
#include <asm/cacheflush.h>
#include <asm/pgtable-prot.h>
@@ -365,6 +367,30 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid)
return set_memory_valid(addr, nr, valid);
}
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+int set_memory_pkey(unsigned long addr, int numpages, int pkey)
+{
+ unsigned long set_prot = 0;
+
+ if (!kpkeys_enabled())
+ return 0;
+
+ if (!__is_lm_address(kasan_reset_tag((void *)addr)))
+ return -EINVAL;
+
+ if (pkey >= arch_max_pkey())
+ return -EINVAL;
+
+ set_prot |= pkey & BIT(0) ? PTE_PO_IDX_0 : 0;
+ set_prot |= pkey & BIT(1) ? PTE_PO_IDX_1 : 0;
+ set_prot |= pkey & BIT(2) ? PTE_PO_IDX_2 : 0;
+
+ return __change_memory_common(addr, PAGE_SIZE * numpages,
+ __pgprot(set_prot),
+ __pgprot(PTE_PO_IDX_MASK));
+}
+#endif
+
#ifdef CONFIG_DEBUG_PAGEALLOC
/*
* This is - apart from the return value - doing the same
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 07/24] arm64: Context-switch POR_EL1
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (5 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 06/24] arm64: set_memory: Implement set_memory_pkey() Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 08/24] arm64: Initialize POR_EL1 register on cpu_resume() Kevin Brodsky
` (16 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
POR_EL1 is about to be used by the kpkeys framework, modifying it
for (typically small) sections of code. If an exception occurs
during that window and scheduling occurs, we must ensure that
POR_EL1 is context-switched as needed (saving the old value and
restoring the new one). An ISB is needed to ensure the write takes
effect, so we skip it if the new value is the same as the old, like
for POR_EL0.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/processor.h | 1 +
arch/arm64/kernel/process.c | 9 +++++++++
2 files changed, 10 insertions(+)
diff --git a/arch/arm64/include/asm/processor.h b/arch/arm64/include/asm/processor.h
index e30c4c8e3a7a..6095322343fc 100644
--- a/arch/arm64/include/asm/processor.h
+++ b/arch/arm64/include/asm/processor.h
@@ -192,6 +192,7 @@ struct thread_struct {
u64 svcr;
u64 tpidr2_el0;
u64 por_el0;
+ u64 por_el1;
#ifdef CONFIG_ARM64_GCS
unsigned int gcs_el0_mode;
unsigned int gcs_el0_locked;
diff --git a/arch/arm64/kernel/process.c b/arch/arm64/kernel/process.c
index 033643cd4e5e..3ec387076588 100644
--- a/arch/arm64/kernel/process.c
+++ b/arch/arm64/kernel/process.c
@@ -466,6 +466,9 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
ptrauth_thread_init_kernel(p);
+ if (system_supports_poe())
+ p->thread.por_el1 = read_sysreg_s(SYS_POR_EL1);
+
if (likely(!args->fn)) {
*childregs = *current_pt_regs();
childregs->regs[0] = 0;
@@ -716,6 +719,12 @@ static void permission_overlay_switch(struct task_struct *next)
* of POR_EL0.
*/
}
+
+ current->thread.por_el1 = read_sysreg_s(SYS_POR_EL1);
+ if (current->thread.por_el1 != next->thread.por_el1) {
+ write_sysreg_s(next->thread.por_el1, SYS_POR_EL1);
+ isb();
+ }
}
/*
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 08/24] arm64: Initialize POR_EL1 register on cpu_resume()
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (6 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 07/24] arm64: Context-switch POR_EL1 Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 09/24] arm64: Enable kpkeys Kevin Brodsky
` (15 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
From: Yeoreum Yun <yeoreum.yun@arm.com>
The POR_EL1 register is reset to an unknown value after cpu_suspend().
Since POR_EL1 always holds POR_EL1_INIT when entering cpu_suspend(),
initialize POR_EL1 with POR_EL1_INIT before cpu_do_resume().
Signed-off-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/kernel/sleep.S | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/arch/arm64/kernel/sleep.S b/arch/arm64/kernel/sleep.S
index f093cdf71be1..e0a6ad85cd24 100644
--- a/arch/arm64/kernel/sleep.S
+++ b/arch/arm64/kernel/sleep.S
@@ -3,6 +3,7 @@
#include <linux/linkage.h>
#include <asm/asm-offsets.h>
#include <asm/assembler.h>
+#include <asm/kpkeys.h>
#include <asm/smp.h>
.text
@@ -134,6 +135,17 @@ SYM_FUNC_START(_cpu_resume)
/* load sp from context */
ldr x2, [x0, #CPU_CTX_SP]
mov sp, x2
+
+#ifdef CONFIG_ARM64_POE
+alternative_if_not ARM64_HAS_S1POE
+ b .Lskip_por_set
+alternative_else_nop_endif
+ mov_q x2, POR_EL1_INIT
+ msr_s SYS_POR_EL1, x2
+ /* isb can be skipped since cpu_do_resume() will do it. */
+.Lskip_por_set:
+#endif /* CONFIG_ARM64_POE */
+
/*
* cpu_do_resume expects x0 to contain context address pointer
*/
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 09/24] arm64: Enable kpkeys
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (7 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 08/24] arm64: Initialize POR_EL1 register on cpu_resume() Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:05 ` [PATCH RFC v7 10/24] memblock: Move INIT_MEMBLOCK_* macros to header Kevin Brodsky
` (14 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
This is the final step to enable kpkeys on arm64. We enable
POE at EL1 by setting TCR2_EL1.POE, and initialise POR_EL1 to the
default value, enabling access to the default pkey/POIndex (0).
An ISB is added so that POE restrictions are enforced immediately.
Having done this, we can now select ARCH_HAS_KPKEYS if ARM64_POE is
enabled.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/Kconfig | 1 +
arch/arm64/kernel/cpufeature.c | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index fe60738e5943..ab06324a50ae 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -2145,6 +2145,7 @@ config ARM64_POE
def_bool y
select ARCH_USES_HIGH_VMA_FLAGS
select ARCH_HAS_PKEYS
+ select ARCH_HAS_KPKEYS
help
The Permission Overlay Extension is used to implement Memory
Protection Keys. Memory Protection Keys provides a mechanism for
diff --git a/arch/arm64/kernel/cpufeature.c b/arch/arm64/kernel/cpufeature.c
index 6d53bb15cf7b..e032322c0c36 100644
--- a/arch/arm64/kernel/cpufeature.c
+++ b/arch/arm64/kernel/cpufeature.c
@@ -76,6 +76,7 @@
#include <linux/kasan.h>
#include <linux/percpu.h>
#include <linux/sched/isolation.h>
+#include <linux/kpkeys.h>
#include <asm/arm_pmuv3.h>
#include <asm/cpu.h>
@@ -2451,8 +2452,10 @@ static void cpu_enable_mops(const struct arm64_cpu_capabilities *__unused)
#ifdef CONFIG_ARM64_POE
static void cpu_enable_poe(const struct arm64_cpu_capabilities *__unused)
{
- sysreg_clear_set(REG_TCR2_EL1, 0, TCR2_EL1_E0POE);
+ write_sysreg_s(POR_EL1_INIT, SYS_POR_EL1);
+ sysreg_clear_set(REG_TCR2_EL1, 0, TCR2_EL1_E0POE | TCR2_EL1_POE);
sysreg_clear_set(CPACR_EL1, 0, CPACR_EL1_E0POE);
+ isb();
}
#endif
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 10/24] memblock: Move INIT_MEMBLOCK_* macros to header
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (8 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 09/24] arm64: Enable kpkeys Kevin Brodsky
@ 2026-05-05 16:05 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 11/24] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature Kevin Brodsky
` (13 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:05 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
The upcoming page table allocator for the kpkeys_hardened_pgtables
feature will need to know the maximum number of memblock regions.
Move the corresponding macros to <linux/memblock.h> to allow that.
INIT_MEMBLOCK_{RESERVED,MEMORY}_REGIONS may be overridden, but this
should be fine as only arm64 and loong currently do that and the
relevant header is already (indirectly) included by
<linux/memblock.h>.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/memblock.h | 11 +++++++++++
mm/memblock.c | 11 -----------
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/include/linux/memblock.h b/include/linux/memblock.h
index b0f750d22a7b..e58b2c859b95 100644
--- a/include/linux/memblock.h
+++ b/include/linux/memblock.h
@@ -24,6 +24,17 @@ extern unsigned long max_pfn;
*/
extern unsigned long long max_possible_pfn;
+#define INIT_MEMBLOCK_REGIONS 128
+#define INIT_PHYSMEM_REGIONS 4
+
+#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
+#define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
+#endif
+
+#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
+#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
+#endif
+
/**
* enum memblock_flags - definition of memory region attributes
* @MEMBLOCK_NONE: no special request
diff --git a/mm/memblock.c b/mm/memblock.c
index a6a1c91e276d..e64ad9c72dc3 100644
--- a/mm/memblock.c
+++ b/mm/memblock.c
@@ -30,17 +30,6 @@
#include "internal.h"
-#define INIT_MEMBLOCK_REGIONS 128
-#define INIT_PHYSMEM_REGIONS 4
-
-#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
-# define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
-#endif
-
-#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
-#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
-#endif
-
/**
* DOC: memblock overview
*
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 11/24] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (9 preceding siblings ...)
2026-05-05 16:05 ` [PATCH RFC v7 10/24] memblock: Move INIT_MEMBLOCK_* macros to header Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 12/24] mm: kpkeys: Protect regular page tables Kevin Brodsky
` (12 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
kpkeys_hardened_pgtables is a hardening feature based on kpkeys. It
aims to prevent the corruption of page tables by: 1. mapping all
page table pages, both kernel and user, with a privileged pkey
(KPKEYS_PKEY_PGTABLES), and 2. granting write access to that pkey
only when running at in a privileged kpkeys context
(KPKEYS_CTX_PGTABLES). This patch introduces basic infrastructure;
the implementation of both aspects will follow.
The feature is exposed as CONFIG_KPKEYS_HARDENED_PGTABLES; it
requires explicit architecture opt-in by selecting
ARCH_HAS_KPKEYS_HARDENED_PGTABLES, since much of the page table
handling is arch-specific.
Because this feature relies on kpkeys being available and enabled,
and modifies attributes of the linear map, it must be inactive on
boot. kpkeys_hardened_pgtables_init() enables it by toggling a
static key; this function must be called by supported architectures
in mem_init(), before any call to pagetable_alloc() is made.
Supported architectures must also provide
arch_supports_kpkeys_early() in <asm/kpkeys.h>. This will be used
during early boot to detect whether kpkeys_hardened_pgtables is
going to be enabled (e.g. to decide how to allocate early page
tables).
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/asm-generic/kpkeys.h | 4 ++++
include/linux/kpkeys.h | 40 +++++++++++++++++++++++++++++++++++++++-
mm/Kconfig | 3 +++
mm/Makefile | 1 +
mm/kpkeys_hardened_pgtables.c | 16 ++++++++++++++++
security/Kconfig.hardening | 12 ++++++++++++
6 files changed, 75 insertions(+), 1 deletion(-)
diff --git a/include/asm-generic/kpkeys.h b/include/asm-generic/kpkeys.h
index ab819f157d6a..cec92334a9f3 100644
--- a/include/asm-generic/kpkeys.h
+++ b/include/asm-generic/kpkeys.h
@@ -2,6 +2,10 @@
#ifndef __ASM_GENERIC_KPKEYS_H
#define __ASM_GENERIC_KPKEYS_H
+#ifndef KPKEYS_PKEY_PGTABLES
+#define KPKEYS_PKEY_PGTABLES 1
+#endif
+
#ifndef KPKEYS_PKEY_DEFAULT
#define KPKEYS_PKEY_DEFAULT 0
#endif
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index cb2d22758391..1ed0299ad5ac 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -4,11 +4,13 @@
#include <linux/bug.h>
#include <linux/cleanup.h>
+#include <linux/jump_label.h>
#define KPKEYS_CTX_DEFAULT 0
+#define KPKEYS_CTX_PGTABLES 1
#define KPKEYS_CTX_MIN KPKEYS_CTX_DEFAULT
-#define KPKEYS_CTX_MAX KPKEYS_CTX_DEFAULT
+#define KPKEYS_CTX_MAX KPKEYS_CTX_PGTABLES
#define __KPKEYS_GUARD(name, set_context, restore_pkey_reg, set_arg, ...) \
__DEFINE_CLASS_IS_CONDITIONAL(name, false); \
@@ -115,4 +117,40 @@ static inline bool kpkeys_enabled(void)
#endif /* CONFIG_ARCH_HAS_KPKEYS */
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+
+DECLARE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
+
+static inline bool kpkeys_hardened_pgtables_enabled(void)
+{
+ return static_branch_unlikely(&kpkeys_hardened_pgtables_key);
+}
+
+static inline bool kpkeys_hardened_pgtables_early_enabled(void)
+{
+ return arch_supports_kpkeys_early();
+}
+
+/*
+ * Should be called from mem_init(): as soon as the buddy allocator becomes
+ * available and before any call to pagetable_alloc().
+ */
+void kpkeys_hardened_pgtables_init(void);
+
+#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
+static inline bool kpkeys_hardened_pgtables_enabled(void)
+{
+ return false;
+}
+
+static inline bool kpkeys_hardened_pgtables_early_enabled(void)
+{
+ return false;
+}
+
+static inline void kpkeys_hardened_pgtables_init(void) {}
+
+#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/Kconfig b/mm/Kconfig
index 819fb0d7b7bd..dbba6b878d5a 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1244,6 +1244,9 @@ config ARCH_HAS_PKEYS
bool
config ARCH_HAS_KPKEYS
bool
+# ARCH_HAS_KPKEYS must be selected when selecting this option
+config ARCH_HAS_KPKEYS_HARDENED_PGTABLES
+ bool
config ARCH_USES_PG_ARCH_2
bool
diff --git a/mm/Makefile b/mm/Makefile
index 8ad2ab08244e..7603e6051afa 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -150,3 +150,4 @@ obj-$(CONFIG_SHRINKER_DEBUG) += shrinker_debug.o
obj-$(CONFIG_EXECMEM) += execmem.o
obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o
obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o
+obj-$(CONFIG_KPKEYS_HARDENED_PGTABLES) += kpkeys_hardened_pgtables.o
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
new file mode 100644
index 000000000000..763f267bbfe4
--- /dev/null
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/kpkeys.h>
+#include <linux/mm.h>
+
+#include <kunit/visibility.h>
+
+__ro_after_init DEFINE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
+EXPORT_SYMBOL_IF_KUNIT(kpkeys_hardened_pgtables_key);
+
+void __init kpkeys_hardened_pgtables_init(void)
+{
+ if (!kpkeys_enabled())
+ return;
+
+ static_branch_enable(&kpkeys_hardened_pgtables_key);
+}
diff --git a/security/Kconfig.hardening b/security/Kconfig.hardening
index 86f8768c63d4..fdaf977d4626 100644
--- a/security/Kconfig.hardening
+++ b/security/Kconfig.hardening
@@ -275,6 +275,18 @@ config BUG_ON_DATA_CORRUPTION
If unsure, say N.
+config KPKEYS_HARDENED_PGTABLES
+ bool "Harden page tables using kernel pkeys"
+ depends on ARCH_HAS_KPKEYS_HARDENED_PGTABLES
+ help
+ This option makes all page tables mostly read-only by
+ allocating them with a non-default protection key (pkey) and
+ only enabling write access to that pkey in routines that are
+ expected to write to page table entries.
+
+ This option has no effect if the system does not support
+ kernel pkeys.
+
endmenu
config CC_HAS_RANDSTRUCT
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 12/24] mm: kpkeys: Protect regular page tables
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (10 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 11/24] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 13/24] mm: kpkeys: Introduce early page table allocator Kevin Brodsky
` (11 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
If the kpkeys_hardened_pgtables feature is enabled, page table pages
(PTPs) should be protected by modifying the linear mapping to map
them with a privileged pkey (KPKEYS_PKEY_PGTABLES). This patch
introduces a new page allocator for that purpose:
* kpkeys_pgtable_alloc() allocates a new PTP and sets the linear
mapping to KPKEYS_PKEY_PGTABLES for that page
* kpkeys_pgtable_free() frees such a PTP and restores the linear
mapping to the default pkey
This interface is then hooked into pagetable_alloc() and
pagetable_free(), protecting all page tables created once the buddy
allocator is available. Early page tables are allocated in other
ways and will be protected in subsequent patches.
This implementation of kpkeys_pgtable_{alloc,free}() is minimal and
relies on the linear map being fully PTE-mapped - otherwise
calling set_memory_pkey() on a single page may result in splitting a
block mapping, which in turn requires allocating a new PTP. A more
elaborate implementation could be added later to handle this
situation.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 10 +++++++++
include/linux/mm.h | 14 +++++++++++--
mm/kpkeys_hardened_pgtables.c | 47 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index 1ed0299ad5ac..c9f63415162b 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -131,6 +131,9 @@ static inline bool kpkeys_hardened_pgtables_early_enabled(void)
return arch_supports_kpkeys_early();
}
+struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order);
+void kpkeys_pgtable_free(struct page *page, unsigned int order);
+
/*
* Should be called from mem_init(): as soon as the buddy allocator becomes
* available and before any call to pagetable_alloc().
@@ -149,6 +152,13 @@ static inline bool kpkeys_hardened_pgtables_early_enabled(void)
return false;
}
+static inline struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
+{
+ return NULL;
+}
+
+static inline void kpkeys_pgtable_free(struct page *page, unsigned int order) {}
+
static inline void kpkeys_hardened_pgtables_init(void) {}
#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
diff --git a/include/linux/mm.h b/include/linux/mm.h
index af23453e9dbd..7b95b2351763 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -37,6 +37,7 @@
#include <linux/bitmap.h>
#include <linux/bitops.h>
#include <linux/iommu-debug-pagealloc.h>
+#include <linux/kpkeys.h>
struct mempolicy;
struct anon_vma;
@@ -3648,7 +3649,12 @@ static inline bool ptdesc_test_kernel(const struct ptdesc *ptdesc)
*/
static inline struct ptdesc *pagetable_alloc_noprof(gfp_t gfp, unsigned int order)
{
- struct page *page = alloc_pages_noprof(gfp | __GFP_COMP, order);
+ struct page *page;
+
+ if (kpkeys_hardened_pgtables_enabled())
+ page = kpkeys_pgtable_alloc(gfp | __GFP_COMP, order);
+ else
+ page = alloc_pages_noprof(gfp | __GFP_COMP, order);
return page_ptdesc(page);
}
@@ -3657,8 +3663,12 @@ static inline struct ptdesc *pagetable_alloc_noprof(gfp_t gfp, unsigned int orde
static inline void __pagetable_free(struct ptdesc *pt)
{
struct page *page = ptdesc_page(pt);
+ unsigned int order = compound_order(page);
- __free_pages(page, compound_order(page));
+ if (kpkeys_hardened_pgtables_enabled())
+ kpkeys_pgtable_free(page, order);
+ else
+ __free_pages(page, order);
}
#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index 763f267bbfe4..fff7e2a64b64 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -1,12 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kpkeys.h>
#include <linux/mm.h>
+#include <linux/set_memory.h>
#include <kunit/visibility.h>
__ro_after_init DEFINE_STATIC_KEY_FALSE(kpkeys_hardened_pgtables_key);
EXPORT_SYMBOL_IF_KUNIT(kpkeys_hardened_pgtables_key);
+static int set_pkey_pgtable(struct page *page, unsigned int nr_pages)
+{
+ unsigned long addr = (unsigned long)page_address(page);
+ int ret;
+
+ ret = set_memory_pkey(addr, nr_pages, KPKEYS_PKEY_PGTABLES);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+static int set_pkey_default(struct page *page, unsigned int nr_pages)
+{
+ unsigned long addr = (unsigned long)page_address(page);
+ int ret;
+
+ ret = set_memory_pkey(addr, nr_pages, KPKEYS_PKEY_DEFAULT);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
+{
+ struct page *page;
+ int ret;
+
+ page = alloc_pages_noprof(gfp, order);
+ if (!page)
+ return page;
+
+ ret = set_pkey_pgtable(page, 1 << order);
+ if (ret) {
+ __free_pages(page, order);
+ return NULL;
+ }
+
+ return page;
+}
+
+void kpkeys_pgtable_free(struct page *page, unsigned int order)
+{
+ set_pkey_default(page, 1 << order);
+ __free_pages(page, order);
+}
+
void __init kpkeys_hardened_pgtables_init(void)
{
if (!kpkeys_enabled())
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 13/24] mm: kpkeys: Introduce early page table allocator
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (11 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 12/24] mm: kpkeys: Protect regular page tables Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 14/24] mm: kpkeys: Protect vmemmap page tables Kevin Brodsky
` (10 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
The kpkeys_hardened_pgtables feature aims to protect all page table
pages (PTPs) by mapping them with a privileged pkey. This is primarily
handled by kpkeys_pgtable_alloc(), called from pagetable_alloc().
However, this does not cover PTPs allocated early, before the
buddy allocator is available. These PTPs are allocated by architecture
code, either 1. from static pools or 2. using the memblock allocator,
and should also be protected.
This patch addresses the second category: PTPs allocated via memblock.
Such PTPs are notably used to create the linear map. Protecting them as
soon as they are allocated would require modifying the linear map while
it is being created, which seems at best difficult. Instead, a
simple allocator is introduced, obtaining pages from memblock and
keeping track of all allocated ranges to set their pkey once it is
safe to do so. PTPs allocated at that stage are not freed, so there
is no need to manage a free list.
Since kpkeys_hardened_pgtables currently requires the linear map to
be PTE-mapped, we can directly allocate page by page using memblock,
without intermediate cache. We rely on memblock allocating
contiguous pages to minimise the number of tracked ranges.
The number of PTPs required to create the linear map is proportional to
the amount of available memory, which means it may be large. At such
an early point, the memblock allocator may however only track a
limited number of regions, and we size the tracking array
(allocated_ranges) accordingly. The array may be quite large as a
result (16KB on arm64), but it is discarded once boot has completed.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 7 +++
mm/kpkeys_hardened_pgtables.c | 116 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 123 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index c9f63415162b..544a2d954bc1 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -140,6 +140,8 @@ void kpkeys_pgtable_free(struct page *page, unsigned int order);
*/
void kpkeys_hardened_pgtables_init(void);
+phys_addr_t kpkeys_physmem_pgtable_alloc(void);
+
#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
static inline bool kpkeys_hardened_pgtables_enabled(void)
@@ -161,6 +163,11 @@ static inline void kpkeys_pgtable_free(struct page *page, unsigned int order) {}
static inline void kpkeys_hardened_pgtables_init(void) {}
+static inline phys_addr_t kpkeys_physmem_pgtable_alloc(void)
+{
+ return 0;
+}
+
#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
#endif /* _LINUX_KPKEYS_H */
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index fff7e2a64b64..c7a8935571ac 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kpkeys.h>
+#include <linux/memblock.h>
#include <linux/mm.h>
#include <linux/set_memory.h>
@@ -30,6 +31,9 @@ static int set_pkey_default(struct page *page, unsigned int nr_pages)
return ret;
}
+/* pkeys physmem allocator (PPA) - implemented below */
+static void ppa_finalize(void);
+
struct page *kpkeys_pgtable_alloc(gfp_t gfp, unsigned int order)
{
struct page *page;
@@ -60,4 +64,116 @@ void __init kpkeys_hardened_pgtables_init(void)
return;
static_branch_enable(&kpkeys_hardened_pgtables_key);
+
+ ppa_finalize();
+}
+
+/*
+ * pkeys physmem allocator (PPA): allocator for very early page tables
+ * (especially for creating the linear map), based on memblock. Allocated
+ * ranges are tracked so that their pkey can be set once it is safe to do so.
+ */
+
+/*
+ * We may have to track many ranges when allocating page tables for the linear
+ * map, as their number grows with the amount of available memory. Assuming that
+ * memblock returns contiguous blocks whenever possible, the number of ranges
+ * to track cannot however exceed the number of regions that memblock itself
+ * tracks. memblock_allow_resize() hasn't been called yet at that point, so
+ * that limit is the size of the statically allocated array.
+ */
+#define PHYSMEM_MAX_RANGES INIT_MEMBLOCK_MEMORY_REGIONS
+
+struct physmem_range {
+ phys_addr_t addr;
+ phys_addr_t size;
+};
+
+struct pkeys_physmem_allocator {
+ struct physmem_range allocated_ranges[PHYSMEM_MAX_RANGES];
+ unsigned int nr_allocated_ranges;
+};
+
+static struct pkeys_physmem_allocator pkeys_physmem_allocator __initdata;
+
+static int __init set_pkey_pgtable_phys(phys_addr_t pa, phys_addr_t size)
+{
+ unsigned long addr = (unsigned long)__va(pa);
+ int ret;
+
+ ret = set_memory_pkey(addr, size / PAGE_SIZE, KPKEYS_PKEY_PGTABLES);
+ pr_debug("%s: addr=%pa, size=%pa\n", __func__, &addr, &size);
+
+ WARN_ON(ret);
+ return ret;
+}
+
+static bool __init ppa_try_extend_last_range(phys_addr_t addr, phys_addr_t size)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+ struct physmem_range *range;
+
+ if (!ppa->nr_allocated_ranges)
+ return false;
+
+ range = &ppa->allocated_ranges[ppa->nr_allocated_ranges - 1];
+
+ /* Merge the new range into the last range if they are contiguous */
+ if (addr == range->addr + range->size) {
+ range->size += size;
+ return true;
+ } else if (addr + size == range->addr) {
+ range->addr -= size;
+ range->size += size;
+ return true;
+ }
+
+ return false;
+}
+
+static void __init ppa_register_allocated_range(phys_addr_t addr,
+ phys_addr_t size)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+ struct physmem_range *range;
+
+ if (!addr)
+ return;
+
+ if (ppa_try_extend_last_range(addr, size))
+ return;
+
+ /* Could not extend the last range, create a new one */
+ if (WARN_ON(ppa->nr_allocated_ranges >= PHYSMEM_MAX_RANGES))
+ return;
+
+ range = &ppa->allocated_ranges[ppa->nr_allocated_ranges++];
+ range->addr = addr;
+ range->size = size;
+}
+
+static void __init ppa_finalize(void)
+{
+ struct pkeys_physmem_allocator *ppa = &pkeys_physmem_allocator;
+
+ for (unsigned int i = 0; i < ppa->nr_allocated_ranges; i++) {
+ struct physmem_range *range = &ppa->allocated_ranges[i];
+
+ set_pkey_pgtable_phys(range->addr, range->size);
+ }
+}
+
+phys_addr_t __ref kpkeys_physmem_pgtable_alloc(void)
+{
+ size_t size = PAGE_SIZE;
+ phys_addr_t addr;
+
+ addr = memblock_phys_alloc_range(size, size, 0,
+ MEMBLOCK_ALLOC_NOLEAKTRACE);
+ if (!addr)
+ return addr;
+
+ ppa_register_allocated_range(addr, size);
+
+ return addr;
}
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 14/24] mm: kpkeys: Protect vmemmap page tables
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (12 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 13/24] mm: kpkeys: Introduce early page table allocator Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 15/24] mm: kpkeys: Introduce hook for protecting static " Kevin Brodsky
` (9 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
When the kpkeys_hardened_pgtables feature is enabled, make sure that
vmemmap page tables are protected by using:
* The standard pagetable_alloc() if the buddy allocator is
available, as it already allocates protected memory.
* The memblock-based kpkeys allocator for early allocations.
These allocators are not NUMA-aware, so the page tables may be
allocated on any node. This could potentially incur some overhead on
large NUMA systems.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
This is a minimal patch to protect vmemmmap page tables. More work
may be needed here:
* Restoring NUMA awareness
* General refactoring of how these page tables are allocated: since
we are not using the standard per-level functions (e.g.
pmd_alloc()), we are not calling pagetable_*_ctor() or
ptdesc_set_kernel(). [Maybe that doesn't matter because these page
tables can only be freed via vmemmap_free()?]
---
mm/sparse-vmemmap.c | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/mm/sparse-vmemmap.c b/mm/sparse-vmemmap.c
index 6eadb9d116e4..0c0d3b1e356c 100644
--- a/mm/sparse-vmemmap.c
+++ b/mm/sparse-vmemmap.c
@@ -184,13 +184,28 @@ pte_t * __meminit vmemmap_pte_populate(pmd_t *pmd, unsigned long addr, int node,
return pte;
}
-static void * __meminit vmemmap_alloc_block_zero(unsigned long size, int node)
+static void * __meminit vmemmap_alloc_pgtable(int node)
{
- void *p = vmemmap_alloc_block(size, node);
+ void *p;
+
+ if (slab_is_available()) {
+ struct ptdesc *ptdesc = pagetable_alloc(GFP_PGTABLE_KERNEL, 0);
+
+ return ptdesc ? ptdesc_address(ptdesc) : NULL;
+ }
+
+ if (kpkeys_hardened_pgtables_early_enabled()) {
+ phys_addr_t phys = kpkeys_physmem_pgtable_alloc();
+
+ p = phys ? phys_to_virt(phys) : NULL;
+ } else {
+ p = __earlyonly_bootmem_alloc(node, PAGE_SIZE, PAGE_SIZE,
+ __pa(MAX_DMA_ADDRESS));
+ }
if (!p)
return NULL;
- memset(p, 0, size);
+ memset(p, 0, PAGE_SIZE);
return p;
}
@@ -199,7 +214,7 @@ pmd_t * __meminit vmemmap_pmd_populate(pud_t *pud, unsigned long addr, int node)
{
pmd_t *pmd = pmd_offset(pud, addr);
if (pmd_none(*pmd)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
kernel_pte_init(p);
@@ -212,7 +227,7 @@ pud_t * __meminit vmemmap_pud_populate(p4d_t *p4d, unsigned long addr, int node)
{
pud_t *pud = pud_offset(p4d, addr);
if (pud_none(*pud)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pmd_init(p);
@@ -225,7 +240,7 @@ p4d_t * __meminit vmemmap_p4d_populate(pgd_t *pgd, unsigned long addr, int node)
{
p4d_t *p4d = p4d_offset(pgd, addr);
if (p4d_none(*p4d)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pud_init(p);
@@ -238,7 +253,7 @@ pgd_t * __meminit vmemmap_pgd_populate(unsigned long addr, int node)
{
pgd_t *pgd = pgd_offset_k(addr);
if (pgd_none(*pgd)) {
- void *p = vmemmap_alloc_block_zero(PAGE_SIZE, node);
+ void *p = vmemmap_alloc_pgtable(node);
if (!p)
return NULL;
pgd_populate_kernel(addr, pgd, p);
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 15/24] mm: kpkeys: Introduce hook for protecting static page tables
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (13 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 14/24] mm: kpkeys: Protect vmemmap page tables Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 16/24] arm64: kpkeys: Implement arch_supports_kpkeys_early() Kevin Brodsky
` (8 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
The kpkeys_hardened_pgtables infrastructure introduced so far allows
compatible architectures to protect all page table pages (PTPs)
allocated at runtime (first via memblock, then the buddy allocator).
Some PTPs are however required even earlier, before any allocator is
available. This is typically needed for mapping the kernel image
itself.
These PTPs are at least as sensitive as those allocated later on,
and should be protected by mapping them with the privileged pkey.
Exactly how these pages are obtained is entirely arch-specific, so
we introduce a hook to let architectures that implement
kpkeys_hardened_pgtables do the right thing.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 4 ++++
mm/kpkeys_hardened_pgtables.c | 1 +
2 files changed, 5 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index 544a2d954bc1..3f7f980f3a7c 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -142,6 +142,10 @@ void kpkeys_hardened_pgtables_init(void);
phys_addr_t kpkeys_physmem_pgtable_alloc(void);
+#ifndef arch_kpkeys_protect_static_pgtables
+static inline void arch_kpkeys_protect_static_pgtables(void) {}
+#endif
+
#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
static inline bool kpkeys_hardened_pgtables_enabled(void)
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index c7a8935571ac..9c6f32741009 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -66,6 +66,7 @@ void __init kpkeys_hardened_pgtables_init(void)
static_branch_enable(&kpkeys_hardened_pgtables_key);
ppa_finalize();
+ arch_kpkeys_protect_static_pgtables();
}
/*
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 16/24] arm64: kpkeys: Implement arch_supports_kpkeys_early()
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (14 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 15/24] mm: kpkeys: Introduce hook for protecting static " Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 17/24] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES Kevin Brodsky
` (7 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
We need to check if the kpkeys_hardened_pgtables feature is going to
be enabled very early during boot, to decide how to set up the
linear map and how to allocate early page tables. This happens even
before boot CPU features are detected.
Implement the arch_supports_kpkeys_early() helper by directly
checking if the boot CPU supports POE, if it is called before boot
CPU features are detected. It may also be called later, in which
case we simply check the POE feature.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/cpufeature.h | 12 ++++++++++++
arch/arm64/include/asm/kpkeys.h | 7 +++++++
2 files changed, 19 insertions(+)
diff --git a/arch/arm64/include/asm/cpufeature.h b/arch/arm64/include/asm/cpufeature.h
index 4de51f8d92cb..8722e9e62702 100644
--- a/arch/arm64/include/asm/cpufeature.h
+++ b/arch/arm64/include/asm/cpufeature.h
@@ -1078,6 +1078,18 @@ static inline bool cpu_has_lpa2(void)
#endif
}
+static inline bool cpu_has_poe(void)
+{
+ u64 mmfr3;
+
+ if (!IS_ENABLED(CONFIG_ARM64_POE))
+ return false;
+
+ mmfr3 = read_sysreg_s(SYS_ID_AA64MMFR3_EL1);
+ return cpuid_feature_extract_unsigned_field(mmfr3,
+ ID_AA64MMFR3_EL1_S1POE_SHIFT);
+}
+
#endif /* __ASSEMBLER__ */
#endif
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
index 7dad3532cacf..ea915f78936b 100644
--- a/arch/arm64/include/asm/kpkeys.h
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -21,6 +21,13 @@ static inline bool arch_supports_kpkeys(void)
return system_supports_poe();
}
+static inline bool arch_supports_kpkeys_early(void)
+{
+ /* POE is a boot feature */
+ return boot_capabilities_finalized() ?
+ system_supports_poe() : cpu_has_poe();
+}
+
#ifdef CONFIG_ARM64_POE
static inline u64 por_set_kpkeys_context(u64 por, int ctx)
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 17/24] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (15 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 16/24] arm64: kpkeys: Implement arch_supports_kpkeys_early() Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 18/24] arm64: kpkeys: Ensure the linear map can be modified Kevin Brodsky
` (6 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Enable RW access to KPKEYS_PKEY_PGTABLES (used to map page table
pages) if switching to KPKEYS_CTX_PGTABLES, otherwise only grant RO
access.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
index ea915f78936b..0c155b970582 100644
--- a/arch/arm64/include/asm/kpkeys.h
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -12,7 +12,8 @@
* Equivalent to por_set_kpkeys_context(0, KPKEYS_CTX_DEFAULT), but can also be
* used in assembly.
*/
-#define POR_EL1_INIT POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX)
+#define POR_EL1_INIT (POR_ELx_PERM_PREP(KPKEYS_PKEY_DEFAULT, POE_RWX) | \
+ POR_ELx_PERM_PREP(KPKEYS_PKEY_PGTABLES, POE_R))
#ifndef __ASSEMBLY__
@@ -33,6 +34,8 @@ static inline bool arch_supports_kpkeys_early(void)
static inline u64 por_set_kpkeys_context(u64 por, int ctx)
{
por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_DEFAULT, POE_RWX);
+ por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_PGTABLES,
+ ctx == KPKEYS_CTX_PGTABLES ? POE_RW : POE_R);
return por;
}
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 18/24] arm64: kpkeys: Ensure the linear map can be modified
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (16 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 17/24] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 19/24] arm64: kpkeys: Protect early page tables Kevin Brodsky
` (5 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
When the kpkeys_hardened_pgtables feature is enabled, we need to be
able to modify attributes (specifically the pkey/POIndex) in the
linear map at page granularity.
Add the appropriate check to can_set_direct_map() on the same
principle as rodata_full and other features.
kpkeys_hardened_pgtables currently requires the linear map to be
fully PTE-mapped, so we also need to ensure that force_pte_mapping()
returns true, like for DEBUG_PAGEALLOC.
Both functions can be called very early, before POE is actually
detected, so the early_enabled() helper is used.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/mm/mmu.c | 2 +-
arch/arm64/mm/pageattr.c | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index 493310cf0486..f79e82c0674a 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -760,7 +760,7 @@ static inline bool force_pte_mapping(void)
const bool bbml2 = system_capabilities_finalized() ?
system_supports_bbml2_noabort() : cpu_supports_bbml2_noabort();
- if (debug_pagealloc_enabled())
+ if (debug_pagealloc_enabled() || kpkeys_hardened_pgtables_early_enabled())
return true;
if (bbml2)
return false;
diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index ecdfaa9701e1..4405f93f1586 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -101,7 +101,8 @@ bool can_set_direct_map(void)
* Realms need to make pages shared/protected at page granularity.
*/
return rodata_full || debug_pagealloc_enabled() ||
- arm64_kfence_can_set_direct_map() || is_realm_world();
+ arm64_kfence_can_set_direct_map() || is_realm_world() ||
+ kpkeys_hardened_pgtables_early_enabled();
}
static int update_range_prot(unsigned long start, unsigned long size,
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 19/24] arm64: kpkeys: Protect early page tables
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (17 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 18/24] arm64: kpkeys: Ensure the linear map can be modified Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 20/24] arm64: kpkeys: Protect init_pg_dir Kevin Brodsky
` (4 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Use the dedicated kpkeys allocator for early page tables (used to create
the linear map) when the kpkeys_hardened_pgtables feature is enabled.
CPU features have not been detected at this stage so we use the
early_enabled() helper. This is not a concern as
kpkeys_physmem_pgtable_alloc() does not itself use POE or
set_memory_pkey().
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/mm/mmu.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index f79e82c0674a..4b9218483dd2 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -109,8 +109,11 @@ static phys_addr_t __init early_pgtable_alloc(enum pgtable_level pgtable_level)
{
phys_addr_t phys;
- phys = memblock_phys_alloc_range(PAGE_SIZE, PAGE_SIZE, 0,
- MEMBLOCK_ALLOC_NOLEAKTRACE);
+ if (kpkeys_hardened_pgtables_early_enabled())
+ phys = kpkeys_physmem_pgtable_alloc();
+ else
+ phys = memblock_phys_alloc_range(PAGE_SIZE, PAGE_SIZE, 0,
+ MEMBLOCK_ALLOC_NOLEAKTRACE);
if (!phys)
panic("Failed to allocate page table page\n");
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 20/24] arm64: kpkeys: Protect init_pg_dir
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (18 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 19/24] arm64: kpkeys: Protect early page tables Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 21/24] arm64: kpkeys: Guard page table writes Kevin Brodsky
` (3 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
When kpkeys_hardened_pgtables is enabled, protect the page tables
that map the kernel image by setting the appropriate pkey for the
linear mapping of those pages.
Most other static page tables (e.g. swapper_pg_dir) should be
read-only both in the kernel image mapping and the linear mapping,
so there is no need to change their pkey.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 7 +++++++
arch/arm64/mm/mmu.c | 13 +++++++++++++
2 files changed, 20 insertions(+)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
index 0c155b970582..71e2035566f4 100644
--- a/arch/arm64/include/asm/kpkeys.h
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -64,6 +64,13 @@ static __always_inline void arch_kpkeys_restore_pkey_reg(u64 pkey_reg)
#endif /* CONFIG_ARM64_POE */
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+
+#define arch_kpkeys_protect_static_pgtables arch_kpkeys_protect_static_pgtables
+void arch_kpkeys_protect_static_pgtables(void);
+
+#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
#endif /* __ASSEMBLY__ */
#endif /* __ASM_KPKEYS_H */
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index 4b9218483dd2..28100ad547e9 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -1055,6 +1055,19 @@ void __init mark_linear_text_alias_ro(void)
PAGE_KERNEL_RO);
}
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+void __init arch_kpkeys_protect_static_pgtables(void)
+{
+ extern char __pi_init_pg_dir[], __pi_init_pg_end[];
+ unsigned long addr = (unsigned long)lm_alias(__pi_init_pg_dir);
+ unsigned long size = __pi_init_pg_end - __pi_init_pg_dir;
+ int ret;
+
+ ret = set_memory_pkey(addr, size / PAGE_SIZE, KPKEYS_PKEY_PGTABLES);
+ WARN_ON(ret);
+}
+#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
#ifdef CONFIG_KFENCE
bool __ro_after_init kfence_early_init = !!CONFIG_KFENCE_SAMPLE_INTERVAL;
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 21/24] arm64: kpkeys: Guard page table writes
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (19 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 20/24] arm64: kpkeys: Protect init_pg_dir Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 22/24] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches Kevin Brodsky
` (2 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
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).
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable.h | 22 +++++++++++++++++++++-
arch/arm64/mm/fault.c | 2 ++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index 4dfa42b7d053..20072f32677d 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)
{
@@ -359,6 +367,7 @@ static inline pte_t pte_clear_uffd_wp(pte_t pte)
static inline void __set_pte_nosync(pte_t *ptep, pte_t pte)
{
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*ptep, pte);
}
@@ -830,6 +839,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))
@@ -894,6 +904,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))
@@ -975,6 +986,7 @@ static inline void set_p4d(p4d_t *p4dp, p4d_t p4d)
return;
}
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*p4dp, p4d);
queue_pte_barriers();
}
@@ -1103,6 +1115,7 @@ static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
return;
}
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pgdp, pgd);
queue_pte_barriers();
}
@@ -1307,6 +1320,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;
@@ -1354,7 +1368,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:
@@ -1427,6 +1444,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);
@@ -1460,6 +1478,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;
@@ -1507,6 +1526,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
diff --git a/arch/arm64/mm/fault.c b/arch/arm64/mm/fault.c
index 0f3c5c7ca054..858f5d3e8f22 100644
--- a/arch/arm64/mm/fault.c
+++ b/arch/arm64/mm/fault.c
@@ -216,6 +216,8 @@ int __ptep_set_access_flags_anysz(struct vm_area_struct *vma,
if (pte_same(pte, entry))
return 0;
+ guard(kpkeys_hardened_pgtables)();
+
/* only preserve the access flags and write permission */
pte_val(entry) &= PTE_RDONLY | PTE_AF | PTE_WRITE | PTE_DIRTY;
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 22/24] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (20 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 21/24] arm64: kpkeys: Guard page table writes Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 23/24] arm64: kpkeys: Enable kpkeys_hardened_pgtables support Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 24/24] mm: Add basic tests for kpkeys_hardened_pgtables Kevin Brodsky
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
The kpkeys_hardened_pgtables feature currently switches kpkeys
context in every helper that writes to page tables, such as
set_pte(). With kpkeys implemented using POE, this entails a pair of
ISBs whenever such helper is called.
A simple way to reduce this overhead is to make use of the lazy MMU
mode. We amend the kpkeys_hardened_pgtables guard so that no kpkeys
context switch (i.e. POR_EL1 update) is issued while the lazy MMU
mode is active. Instead, we switch to KPKEYS_CTX_PGTABLES when
entering the lazy MMU mode, and restore the previous context when
exiting it.
Restoring the previous kpkeys context requires storing the original
value of POR_EL1 somewhere. This is a full 64-bit value so we cannot
simply use a TIF flag. There is no straightforward way to reuse
current->thread.por_el1 for that purpose - this is where the current
value of POR_EL1 is stored on a context switch, i.e. the value
corresponding to KPKEYS_CTX_PGTABLES inside a lazy_mmu section.
Instead, we add a new member to thread_struct to hold that value
temporarily. This isn't optimal as that member is unused outside of
lazy MMU sections, but it is the simplest option. Nesting of
sections is not a concern as arch_{enter,leave}_lazy_mmu_mode() are
not called in inner sections (nor do we need to do anything there).
A further optimisation this patch makes is to merge the ISBs when
exiting lazy_mmu mode. That is, if an ISB is going to be issued by
emit_pte_barriers() because kernel pgtables were modified in the
lazy MMU section, we skip the ISB after restoring POR_EL1. This is
done by checking TIF_LAZY_MMU_PENDING and ensuring that POR_EL1 is
restored before emit_pte_barriers() is called.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable.h | 50 +++++++++++++++++++++++++++++++++++---
arch/arm64/include/asm/processor.h | 1 +
2 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index 20072f32677d..1c0dcfd14678 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -43,10 +43,44 @@
#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
KPKEYS_GUARD_COND(kpkeys_hardened_pgtables, KPKEYS_CTX_PGTABLES,
- kpkeys_hardened_pgtables_enabled())
-#else
+ kpkeys_hardened_pgtables_enabled() &&
+ !is_lazy_mmu_mode_active())
+
+static void kpkeys_lazy_mmu_enter(void)
+{
+ if (!kpkeys_hardened_pgtables_enabled())
+ return;
+
+ current->thread.por_el1_lazy_mmu = kpkeys_set_context(KPKEYS_CTX_PGTABLES);
+}
+
+static void kpkeys_lazy_mmu_exit(void)
+{
+ u64 saved_por_el1;
+
+ if (!kpkeys_hardened_pgtables_enabled())
+ return;
+
+ saved_por_el1 = current->thread.por_el1_lazy_mmu;
+
+ /*
+ * We skip any barrier if TIF_LAZY_MMU_PENDING is set:
+ * emit_pte_barriers() will issue an ISB just after this function
+ * returns.
+ */
+ if (test_thread_flag(TIF_LAZY_MMU_PENDING))
+ __kpkeys_set_pkey_reg_nosync(saved_por_el1);
+ else
+ arch_kpkeys_restore_pkey_reg(saved_por_el1);
+}
+#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
KPKEYS_GUARD_NOOP(kpkeys_hardened_pgtables)
-#endif
+
+static void kpkeys_lazy_mmu_enter(void) {}
+static void kpkeys_lazy_mmu_exit(void) {}
+#endif /* CONFIG_KPKEYS_HARDENED_PGTABLES */
+
+
static inline void emit_pte_barriers(void)
{
@@ -79,7 +113,10 @@ static inline void queue_pte_barriers(void)
}
}
-static inline void arch_enter_lazy_mmu_mode(void) {}
+static inline void arch_enter_lazy_mmu_mode(void)
+{
+ kpkeys_lazy_mmu_enter();
+}
static inline void arch_flush_lazy_mmu_mode(void)
{
@@ -89,6 +126,11 @@ static inline void arch_flush_lazy_mmu_mode(void)
static inline void arch_leave_lazy_mmu_mode(void)
{
+ /*
+ * The ordering should be preserved to allow kpkeys_lazy_mmu_exit()
+ * to skip any barrier when TIF_LAZY_MMU_PENDING is set.
+ */
+ kpkeys_lazy_mmu_exit();
arch_flush_lazy_mmu_mode();
}
diff --git a/arch/arm64/include/asm/processor.h b/arch/arm64/include/asm/processor.h
index 6095322343fc..c3a86ddce637 100644
--- a/arch/arm64/include/asm/processor.h
+++ b/arch/arm64/include/asm/processor.h
@@ -193,6 +193,7 @@ struct thread_struct {
u64 tpidr2_el0;
u64 por_el0;
u64 por_el1;
+ u64 por_el1_lazy_mmu;
#ifdef CONFIG_ARM64_GCS
unsigned int gcs_el0_mode;
unsigned int gcs_el0_locked;
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 23/24] arm64: kpkeys: Enable kpkeys_hardened_pgtables support
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (21 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 22/24] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
2026-05-05 16:06 ` [PATCH RFC v7 24/24] mm: Add basic tests for kpkeys_hardened_pgtables Kevin Brodsky
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
The kpkeys_hardened_pgtables feature needs to be initialised as soon
as the buddy allocator becomes available. The canonical place to
handle this is mem_init().
With that done, all the bits are in place and we can advertise
support for kpkeys_hardened_pgtables by selecting
ARCH_HAS_KPKEYS_HARDENED_PGTABLES if ARM64_POE is enabled.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/Kconfig | 1 +
arch/arm64/mm/init.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index ab06324a50ae..43d3ab744535 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -2146,6 +2146,7 @@ config ARM64_POE
select ARCH_USES_HIGH_VMA_FLAGS
select ARCH_HAS_PKEYS
select ARCH_HAS_KPKEYS
+ select ARCH_HAS_KPKEYS_HARDENED_PGTABLES
help
The Permission Overlay Extension is used to implement Memory
Protection Keys. Memory Protection Keys provides a mechanism for
diff --git a/arch/arm64/mm/init.c b/arch/arm64/mm/init.c
index 97987f850a33..7985045875cf 100644
--- a/arch/arm64/mm/init.c
+++ b/arch/arm64/mm/init.c
@@ -386,6 +386,7 @@ bool page_alloc_available __ro_after_init;
void __init mem_init(void)
{
page_alloc_available = true;
+ kpkeys_hardened_pgtables_init();
swiotlb_update_mem_attributes();
}
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH RFC v7 24/24] mm: Add basic tests for kpkeys_hardened_pgtables
2026-05-05 16:05 [PATCH RFC v7 00/24] pkeys-based page table hardening Kevin Brodsky
` (22 preceding siblings ...)
2026-05-05 16:06 ` [PATCH RFC v7 23/24] arm64: kpkeys: Enable kpkeys_hardened_pgtables support Kevin Brodsky
@ 2026-05-05 16:06 ` Kevin Brodsky
23 siblings, 0 replies; 25+ messages in thread
From: Kevin Brodsky @ 2026-05-05 16:06 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Andy Lutomirski, Catalin Marinas,
Dave Hansen, David Hildenbrand (Arm), Ira Weiny, Jann Horn,
Jeff Xu, Joey Gouly, Kees Cook, Linus Walleij, Marc Zyngier,
Mark Brown, Matthew Wilcox, Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Quentin Perret, Rick Edgecombe,
Ryan Roberts, Will Deacon, Yang Shi, Yeoreum Yun,
linux-arm-kernel, linux-mm, x86, Lorenzo Stoakes, Thomas Gleixner,
Vlastimil Babka
Add basic tests for the kpkeys_hardened_pgtables feature: try to
perform direct writes to kernel and user page table entries and
ensure they fail.
Multiple cases are considered for kernel page tables, as early page
tables are allocated and/or protected in a different way.
The tests are builtin (cannot be built as a module) because they
refer to multiple symbols that are not exported (e.g.
copy_to_kernel_nofault()).
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
mm/Makefile | 1 +
mm/tests/kpkeys_hardened_pgtables_kunit.c | 198 ++++++++++++++++++++++++++++++
security/Kconfig.hardening | 12 ++
3 files changed, 211 insertions(+)
diff --git a/mm/Makefile b/mm/Makefile
index 7603e6051afa..9ebdbaa696b2 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -151,3 +151,4 @@ obj-$(CONFIG_EXECMEM) += execmem.o
obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o
obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o
obj-$(CONFIG_KPKEYS_HARDENED_PGTABLES) += kpkeys_hardened_pgtables.o
+obj-$(CONFIG_KPKEYS_HARDENED_PGTABLES_KUNIT_TEST) += tests/kpkeys_hardened_pgtables_kunit.o
diff --git a/mm/tests/kpkeys_hardened_pgtables_kunit.c b/mm/tests/kpkeys_hardened_pgtables_kunit.c
new file mode 100644
index 000000000000..dd4acdfd4763
--- /dev/null
+++ b/mm/tests/kpkeys_hardened_pgtables_kunit.c
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <kunit/test.h>
+#include <linux/mman.h>
+#include <linux/pgtable.h>
+#include <linux/set_memory.h>
+#include <linux/vmalloc.h>
+
+static void free_page_wrapper(void *ctx)
+{
+ __free_page((struct page *)ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(vfree_wrapper, vfree, const void *);
+
+static pud_t *pud_off_k(unsigned long va)
+{
+ return pud_offset(p4d_offset(pgd_offset_k(va), va), va);
+}
+
+static pte_t *get_kernel_pte(unsigned long addr)
+{
+ pmd_t *pmdp = pmd_off_k(addr);
+
+ if (!pmdp || pmd_leaf(*pmdp))
+ return NULL;
+
+ return pte_offset_kernel(pmdp, addr);
+}
+
+#define write_pgtable(type, ptr) do { \
+ type##_t val; \
+ int ret; \
+ \
+ pr_debug("%s: writing to "#type" at %px\n", __func__, (ptr)); \
+ \
+ val = type##p_get(ptr); \
+ ret = copy_to_kernel_nofault(ptr, &val, sizeof(val)); \
+ KUNIT_EXPECT_EQ_MSG(test, ret, -EFAULT, \
+ "Direct "#type" write wasn't prevented"); \
+} while (0)
+
+/*
+ * Try to write linear map page tables, at every level. This is worthwhile
+ * because those page table pages are obtained from different allocators:
+ *
+ * - Static memory (part of the kernel image) for PGD
+ * - memblock for PUD and possibly PMD/PTE
+ * - pagetable_alloc() (buddy allocator) for PMD/PTE if large block mappings are
+ * used and the linear map is split after being created
+ */
+static void write_direct_map_pgtables(struct kunit *test)
+{
+ struct page *page;
+ unsigned long addr;
+ pgd_t *pgdp;
+ p4d_t *p4dp;
+ pud_t *pudp;
+ pmd_t *pmdp;
+ pte_t *ptep;
+ int ret;
+
+ if (!kpkeys_enabled())
+ kunit_skip(test, "kpkeys are not supported");
+
+ page = alloc_page(GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ ret = kunit_add_action_or_reset(test, free_page_wrapper, page);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Ensure page is PTE-mapped (splitting the linear map if necessary) */
+ ret = set_direct_map_invalid_noflush(page);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = set_direct_map_default_noflush(page);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ addr = (unsigned long)page_address(page);
+
+ pgdp = pgd_offset_k(addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pgdp, "Failed to get PGD");
+ /*
+ * swapper_pg_dir is still writable at this stage, so don't check it.
+ * It is not protected by kpkeys_hardened_pgtables because it should be
+ * made read-only by mark_rodata_ro(). However since these
+ * KUnit tests are builtin, they are run before mark_rodata_ro() is
+ * called.
+ */
+
+ p4dp = p4d_offset(pgdp, addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, p4dp, "Failed to get P4D");
+ /* Not checked; same rationale as PGD in case P4D is folded */
+
+ pudp = pud_offset(p4dp, addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pudp, "Failed to get PUD");
+ write_pgtable(pud, pudp);
+
+ pmdp = pmd_offset(pudp, addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pmdp, "Failed to get PMD");
+ write_pgtable(pmd, pmdp);
+
+ ptep = pte_offset_kernel(pmdp, addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, ptep, "Failed to get PTE");
+ write_pgtable(pte, ptep);
+}
+
+/* Worth checking since the kernel image is mapped with static page tables */
+static void write_kernel_image_pud(struct kunit *test)
+{
+ pud_t *pudp;
+
+ if (!kpkeys_enabled())
+ kunit_skip(test, "kpkeys are not supported");
+
+ /* The kernel is probably block-mapped, check the PUD to be safe */
+ pudp = pud_off_k((unsigned long)&init_mm);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pudp, "Failed to get PUD");
+
+ write_pgtable(pud, pudp);
+}
+
+static void write_kernel_vmalloc_pte(struct kunit *test)
+{
+ void *mem;
+ pte_t *ptep;
+ int ret;
+
+ if (!kpkeys_enabled())
+ kunit_skip(test, "kpkeys are not supported");
+
+ mem = vmalloc(PAGE_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, mem);
+ ret = kunit_add_action_or_reset(test, vfree_wrapper, mem);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* vmalloc() without VM_ALLOW_HUGE_VMAP is PTE-mapped */
+ ptep = get_kernel_pte((unsigned long)mem);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, ptep, "Failed to get PTE");
+
+ write_pgtable(pte, ptep);
+}
+
+static void write_vmemmap_pmd(struct kunit *test)
+{
+ struct page *page;
+ pmd_t *pmdp;
+
+ if (!kpkeys_enabled())
+ kunit_skip(test, "kpkeys are not supported");
+
+ /*
+ * We just need the address of some struct page, so we can free the
+ * page right away.
+ */
+ page = alloc_page(GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ __free_page(page);
+
+ /* vmemmap may use PMD block mappings */
+ pmdp = pmd_off_k((unsigned long)page);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pmdp, "Failed to get PMD");
+ write_pgtable(pmd, pmdp);
+}
+
+static void write_user_pmd(struct kunit *test)
+{
+ pmd_t *pmdp;
+ unsigned long uaddr;
+
+ if (!kpkeys_enabled())
+ kunit_skip(test, "kpkeys are not supported");
+
+ uaddr = kunit_vm_mmap(test, NULL, 0, PAGE_SIZE, PROT_READ,
+ MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, 0);
+ KUNIT_ASSERT_NE_MSG(test, uaddr, 0, "Could not create userspace mm");
+
+ /* We passed MAP_POPULATE so a PMD should already be allocated */
+ pmdp = pmd_off(current->mm, uaddr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pmdp, "Failed to get PMD");
+
+ write_pgtable(pmd, pmdp);
+}
+
+static struct kunit_case kpkeys_hardened_pgtables_test_cases[] = {
+ KUNIT_CASE(write_direct_map_pgtables),
+ KUNIT_CASE(write_kernel_image_pud),
+ KUNIT_CASE(write_kernel_vmalloc_pte),
+ KUNIT_CASE(write_vmemmap_pmd),
+ KUNIT_CASE(write_user_pmd),
+ {}
+};
+
+static struct kunit_suite kpkeys_hardened_pgtables_test_suite = {
+ .name = "kpkeys_hardened_pgtables",
+ .test_cases = kpkeys_hardened_pgtables_test_cases,
+};
+kunit_test_suite(kpkeys_hardened_pgtables_test_suite);
+
+MODULE_DESCRIPTION("Tests for the kpkeys_hardened_pgtables feature");
+MODULE_LICENSE("GPL");
diff --git a/security/Kconfig.hardening b/security/Kconfig.hardening
index fdaf977d4626..48789f93e933 100644
--- a/security/Kconfig.hardening
+++ b/security/Kconfig.hardening
@@ -287,6 +287,18 @@ config KPKEYS_HARDENED_PGTABLES
This option has no effect if the system does not support
kernel pkeys.
+config KPKEYS_HARDENED_PGTABLES_KUNIT_TEST
+ bool "KUnit tests for kpkeys_hardened_pgtables" if !KUNIT_ALL_TESTS
+ depends on KPKEYS_HARDENED_PGTABLES
+ depends on KUNIT=y
+ default KUNIT_ALL_TESTS
+ help
+ Enable this option to check that the kpkeys_hardened_pgtables feature
+ functions as intended, i.e. prevents arbitrary writes to user and
+ kernel page tables.
+
+ If unsure, say N.
+
endmenu
config CC_HAS_RANDSTRUCT
--
2.51.2
^ permalink raw reply related [flat|nested] 25+ messages in thread