* [PATCH RFC v10 01/22] mm: Introduce kpkeys
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 02/22] set_memory: Introduce set_direct_map_pkey() stub Kevin Brodsky
` (20 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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 pair of functions to enter/leave a kpkeys
context, and macros to define guard objects.
Each kpkeys context specifies permissions for the pkeys managed by the
framework. kpkeys_enter_context() applies those permissions and
returns a state for kpkeys_leave_context() to restore.
The saved state is represented by struct kpkeys_state, with one
field for the arch state (typically the saved pkeys register value).
The other field is used to skip the restore step in case we were
already in the context passed to enter_context().
Support for kpkeys must be indicated by selecting ARCH_HAS_KPKEYS.
The architecture must then provide:
* struct arch_kpkeys_state in <asm/kpkeys_types.h>
* arch_kpkeys_enter_context() in <asm/kpkeys.h>:
if needed, sets the new state and saves the original one in
arch_kpkeys_state; reports whether the context was actually
changed
* arch_kpkeys_leave_context() in <asm/kpkeys.h>:
restores the original state
* arch_supports_kpkeys() in <asm/kpkeys.h>:
returns whether the system has an appropriate hardware feature to
support kpkeys
To start with, only KPKEYS_CTX_DEFAULT is available, granting
RW access to the default pkey (0).
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 107 +++++++++++++++++++++++++++++++++++++++++++
include/linux/kpkeys_types.h | 25 ++++++++++
mm/Kconfig | 2 +
3 files changed, 134 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
new file mode 100644
index 000000000000..69ec4808d850
--- /dev/null
+++ b/include/linux/kpkeys.h
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_KPKEYS_H
+#define _LINUX_KPKEYS_H
+
+#include <linux/bug.h>
+#include <linux/cleanup.h>
+#include <linux/kpkeys_types.h>
+
+/**
+ * KPKEYS_GUARD_NOOP() - helper to define a guard type that does nothing
+ * @name: the name of the guard type
+ *
+ * Define a guard type that does nothing, useful to match a real guard type
+ * that is defined under an #ifdef.
+ */
+#define KPKEYS_GUARD_NOOP(name) \
+ __DEFINE_CLASS_IS_CONDITIONAL(name, false); \
+ DEFINE_CLASS(name, bool, (void)_T, false, void); \
+ static inline void *class_##name##_lock_ptr(bool *_T) \
+ { return _T; }
+
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+
+#include <asm/kpkeys.h>
+
+/**
+ * KPKEYS_GUARD_COND() - helper to 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
+ *
+ * Define a guard type that switches to @ctx if @cond evaluates to true,
+ * and does nothing otherwise.
+ */
+#define KPKEYS_GUARD_COND(name, ctx, cond) \
+ __DEFINE_CLASS_IS_CONDITIONAL(name, false); \
+ DEFINE_CLASS(name, struct kpkeys_state, \
+ kpkeys_leave_context(&_T), \
+ (cond) ? kpkeys_enter_context(ctx) : \
+ (struct kpkeys_state) {}, void); \
+ static inline \
+ void *class_##name##_lock_ptr(struct kpkeys_state *_T) \
+ { return _T; }
+
+/**
+ * KPKEYS_GUARD() - helper to define a guard type that switches to a given
+ * kpkeys context if kpkeys are supported
+ * @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_supported())
+
+/**
+ * kpkeys_enter_context() - enter a kpkeys context
+ * @ctx: the context to switch to
+ *
+ * Enters the specified kpkeys context. @ctx must be a compile-time constant.
+ *
+ * Return: state to be passed to kpkeys_leave_context().
+ */
+static __always_inline
+struct kpkeys_state kpkeys_enter_context(enum kpkeys_ctx ctx)
+{
+ struct kpkeys_state state = {};
+
+ BUILD_BUG_ON_MSG(!__builtin_constant_p(ctx),
+ "kpkeys_enter_context() only takes constant values");
+ BUILD_BUG_ON_MSG(ctx < 0 || ctx >= KPKEYS_CTX_COUNT,
+ "Invalid value passed to kpkeys_enter_context()");
+
+ state.entered_context = arch_kpkeys_enter_context(ctx, &state.arch_state);
+ return state;
+}
+
+/**
+ * kpkeys_leave_context() - leave a kpkeys context
+ * @state: state returned by kpkeys_enter_context()
+ *
+ * Restores the state saved when entering a kpkeys context. If no context was
+ * entered, this function does nothing.
+ */
+static __always_inline
+void kpkeys_leave_context(const struct kpkeys_state *state)
+{
+ if (state->entered_context)
+ arch_kpkeys_leave_context(&state->arch_state);
+}
+
+static inline bool kpkeys_supported(void)
+{
+ return arch_supports_kpkeys();
+}
+
+#else /* CONFIG_ARCH_HAS_KPKEYS */
+
+static inline bool kpkeys_supported(void)
+{
+ return false;
+}
+
+#endif /* CONFIG_ARCH_HAS_KPKEYS */
+
+#endif /* _LINUX_KPKEYS_H */
diff --git a/include/linux/kpkeys_types.h b/include/linux/kpkeys_types.h
new file mode 100644
index 000000000000..9de59092e9c3
--- /dev/null
+++ b/include/linux/kpkeys_types.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_KPKEYS_TYPES_H
+#define _LINUX_KPKEYS_TYPES_H
+
+#include <linux/types.h>
+
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+#include <asm/kpkeys_types.h>
+#endif
+
+#if defined(CONFIG_ARCH_HAS_KPKEYS) && !defined(__ASSEMBLY__)
+
+enum kpkeys_ctx {
+ KPKEYS_CTX_DEFAULT = 0,
+ KPKEYS_CTX_COUNT,
+};
+
+struct kpkeys_state {
+ bool entered_context;
+ struct arch_kpkeys_state arch_state;
+};
+
+#endif /* CONFIG_ARCH_HAS_KPKEYS && !__ASSEMBLY__ */
+
+#endif /* _LINUX_KPKEYS_TYPES_H */
diff --git a/mm/Kconfig b/mm/Kconfig
index afff1b25b543..4635628de622 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1265,6 +1265,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 02/22] set_memory: Introduce set_direct_map_pkey() stub
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 01/22] mm: Introduce kpkeys Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 03/22] arm64: Introduce por_elx_set_pkey_perms() helper Kevin Brodsky
` (19 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Introduce a new function, set_direct_map_pkey(), which sets the
protection key (pkey) of pages in the specified direct map range.
Architectures implementing kernel pkeys (kpkeys) must provide a
suitable implementation; an empty stub is added as fallback.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 27c32fbabfec..6172f1ecad35 100644
--- a/include/linux/set_memory.h
+++ b/include/linux/set_memory.h
@@ -190,4 +190,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_direct_map_pkey(unsigned long addr, int numpages, int pkey)
+{
+ return 0;
+}
+#endif
+
#endif /* _LINUX_SET_MEMORY_H_ */
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 03/22] arm64: Introduce por_elx_set_pkey_perms() helper
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 01/22] mm: Introduce kpkeys Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 02/22] set_memory: Introduce set_direct_map_pkey() stub Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 04/22] arm64: Implement asm/kpkeys.h using POE Kevin Brodsky
` (18 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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(). Use field_prep() to encode the new
permissions because the field mask depends on the pkey index.
new_por now represents the permissions of a single pkey, so rename
it to new_perms and change its type accordingly.
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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..66d8c9a73224 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, u8 perms)
+{
+ u64 mask = POE_MASK << POR_ELx_PERM_SHIFT(pkey);
+
+ return (por & ~mask) | field_prep(mask, perms);
+}
+
#endif /* _ASM_ARM64_POR_H */
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index 140520b351e2..6a2155db3d05 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -2355,8 +2355,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;
+ u8 new_perms;
+ u64 por;
if (!system_supports_poe())
return -ENOSPC;
@@ -2370,25 +2370,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 04/22] arm64: Implement asm/kpkeys.h using POE
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (2 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 03/22] arm64: Introduce por_elx_set_pkey_perms() helper Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 05/22] arm64: set_memory: Implement set_direct_map_pkey() Kevin Brodsky
` (17 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Implement the kpkeys interface if CONFIG_ARM64_POE is enabled.
The permissions for pkey 0 are set to RWX as this pkey is also used
for code mappings.
arch_kpkeys_enter_context() returns whether it modified POR_EL1, and
saves its old value when it does. arch_kpkeys_leave_context() is
only called if POR_EL1 actually needs to be restored.
To allow <asm/kpkeys.h> to be included from assembly, also add
appropriate #ifdefs to <asm/por.h>.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/kpkeys.h | 73 +++++++++++++++++++++++++++++++++++
arch/arm64/include/asm/kpkeys_types.h | 15 +++++++
arch/arm64/include/asm/por.h | 4 ++
3 files changed, 92 insertions(+)
diff --git a/arch/arm64/include/asm/kpkeys.h b/arch/arm64/include/asm/kpkeys.h
new file mode 100644
index 000000000000..4f9bdb369b47
--- /dev/null
+++ b/arch/arm64/include/asm/kpkeys.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_KPKEYS_H
+#define __ASM_KPKEYS_H
+
+#include <linux/kpkeys_types.h>
+
+#include <asm/barrier.h>
+#include <asm/cpufeature.h>
+#include <asm/por.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(0, 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, enum kpkeys_ctx ctx)
+{
+ por = por_elx_set_pkey_perms(por, 0, POE_RWX);
+
+ return por;
+}
+
+static __always_inline void __kpkeys_set_pkey_reg_nosync(u64 pkey_reg)
+{
+ /*
+ * Writing to POR_EL1 potentially affects all memory accesses, so
+ * use barrier() to ensure load/store instructions cannot be reordered
+ * past the write.
+ */
+ barrier();
+ write_sysreg_s(pkey_reg, SYS_POR_EL1);
+ barrier();
+}
+
+static __always_inline
+bool arch_kpkeys_enter_context(enum kpkeys_ctx ctx,
+ struct arch_kpkeys_state *state)
+{
+ const u64 prev_por = read_sysreg_s(SYS_POR_EL1);
+ const u64 new_por = por_set_kpkeys_context(prev_por, ctx);
+
+ if (prev_por == new_por)
+ return false;
+
+ state->por = prev_por;
+ __kpkeys_set_pkey_reg_nosync(new_por);
+ isb();
+
+ return true;
+}
+
+static __always_inline
+void arch_kpkeys_leave_context(const struct arch_kpkeys_state *state)
+{
+ __kpkeys_set_pkey_reg_nosync(state->por);
+ isb();
+}
+
+#endif /* CONFIG_ARM64_POE */
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* __ASM_KPKEYS_H */
diff --git a/arch/arm64/include/asm/kpkeys_types.h b/arch/arm64/include/asm/kpkeys_types.h
new file mode 100644
index 000000000000..e9c59a26ee7a
--- /dev/null
+++ b/arch/arm64/include/asm/kpkeys_types.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __ASM_KPKEYS_TYPES_H
+#define __ASM_KPKEYS_TYPES_H
+
+#include <linux/types.h>
+
+#ifndef __ASSEMBLY__
+
+struct arch_kpkeys_state {
+ u64 por;
+};
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* __ASM_KPKEYS_TYPES_H */
diff --git a/arch/arm64/include/asm/por.h b/arch/arm64/include/asm/por.h
index 66d8c9a73224..b89be3cf0a0c 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, u8 perms)
return (por & ~mask) | field_prep(mask, perms);
}
+#endif /* __ASSEMBLY__ */
+
#endif /* _ASM_ARM64_POR_H */
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 05/22] arm64: set_memory: Implement set_direct_map_pkey()
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (3 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 04/22] arm64: Implement asm/kpkeys.h using POE Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 06/22] arm64: Disable EL2 trapping of POR_EL1 Kevin Brodsky
` (16 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Implement set_direct_map_pkey() using POE.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 0091ba12200e..8322615b8ffb 100644
--- a/arch/arm64/include/asm/set_memory.h
+++ b/arch/arm64/include/asm/set_memory.h
@@ -18,4 +18,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_direct_map_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 de7bf95003ea..7143bb2dfecd 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>
@@ -319,6 +321,30 @@ int __set_memory_enc_dec(unsigned long addr, int numpages, bool encrypt)
__pgprot(PTE_PRESENT_INVALID));
}
+#ifdef CONFIG_ARCH_HAS_KPKEYS
+int set_direct_map_pkey(unsigned long addr, int numpages, int pkey)
+{
+ unsigned long set_prot = 0;
+
+ if (!kpkeys_supported() || !can_set_direct_map())
+ return 0;
+
+ if (!__is_lm_address(kasan_reset_tag((void *)addr)))
+ return -EINVAL;
+
+ if (pkey < 0 || 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
void __kernel_map_pages(struct page *page, int numpages, int enable)
{
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 06/22] arm64: Disable EL2 trapping of POR_EL1
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (4 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 05/22] arm64: set_memory: Implement set_direct_map_pkey() Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 07/22] arm64: Initialize POR_EL1 register on cpu_resume() Kevin Brodsky
` (15 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Make sure EL2 doesn't trap kernel accesses to POR_EL1 by setting the
nPOR_EL1 bit in HFGRT{R,W}_EL2.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/el2_setup.h | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/include/asm/el2_setup.h b/arch/arm64/include/asm/el2_setup.h
index 12a500181291..a55e8d5f1eb8 100644
--- a/arch/arm64/include/asm/el2_setup.h
+++ b/arch/arm64/include/asm/el2_setup.h
@@ -380,7 +380,8 @@
ubfx x1, x1, #ID_AA64MMFR3_EL1_S1POE_SHIFT, #4
cbz x1, .Lskip_poe_fgt_\@
- /* Disable trapping of POR_EL0 */
+ /* Disable trapping of POR_ELx */
+ orr x0, x0, #HFGRTR_EL2_nPOR_EL1
orr x0, x0, #HFGRTR_EL2_nPOR_EL0
.Lskip_poe_fgt_\@:
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 07/22] arm64: Initialize POR_EL1 register on cpu_resume()
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (5 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 06/22] arm64: Disable EL2 trapping of POR_EL1 Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 08/22] arm64: Context-switch POR_EL1 Kevin Brodsky
` (14 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 08/22] arm64: Context-switch POR_EL1
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (6 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 07/22] arm64: Initialize POR_EL1 register on cpu_resume() Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 09/22] arm64: mm: Enable overlays for all EL1 indirect permissions Kevin Brodsky
` (13 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Reviewed-by: Linu Cherian <linu.cherian@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/processor.h | 1 +
arch/arm64/kernel/process.c | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/arch/arm64/include/asm/processor.h b/arch/arm64/include/asm/processor.h
index 481f1999f7c1..18424d91d8b2 100644
--- a/arch/arm64/include/asm/processor.h
+++ b/arch/arm64/include/asm/processor.h
@@ -195,6 +195,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 42b15b0c069f..a3984aedfebe 100644
--- a/arch/arm64/kernel/process.c
+++ b/arch/arm64/kernel/process.c
@@ -51,6 +51,7 @@
#include <asm/exec.h>
#include <asm/fpsimd.h>
#include <asm/gcs.h>
+#include <asm/kpkeys.h>
#include <asm/mmu_context.h>
#include <asm/mpam.h>
#include <asm/mte.h>
@@ -446,6 +447,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 = POR_EL1_INIT;
+
if (likely(!args->fn)) {
*childregs = *current_pt_regs();
childregs->regs[0] = 0;
@@ -698,6 +702,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 09/22] arm64: mm: Enable overlays for all EL1 indirect permissions
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (7 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 08/22] arm64: Context-switch POR_EL1 Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 10/22] arm64: Enable kpkeys Kevin Brodsky
` (12 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
In preparation of using POE inside the kernel, enable "Overlay
applied" for kernel memory types in PIR_EL1. This ensures that the
permissions set in POR_EL1 affect all kernel mappings.
User memory types must be left untouched (overlays not applied)
because any privileged access to user memory (e.g. futex atomic
without FEAT_LSUI) would then be mistakenly checked against POR_EL1.
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Reviewed-by: Linu Cherian <linu.cherian@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable-prot.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable-prot.h b/arch/arm64/include/asm/pgtable-prot.h
index 590ef4328f12..eb779c0e892d 100644
--- a/arch/arm64/include/asm/pgtable-prot.h
+++ b/arch/arm64/include/asm/pgtable-prot.h
@@ -183,9 +183,9 @@ static inline bool __pure lpa2_is_enabled(void)
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_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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 10/22] arm64: Enable kpkeys
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (8 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 09/22] arm64: mm: Enable overlays for all EL1 indirect permissions Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 11/22] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature Kevin Brodsky
` (11 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 1075bc661833..f7e4ffbc07ac 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -2300,6 +2300,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 34c5c03c0c9b..42d693197739 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>
@@ -2334,8 +2335,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 11/22] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (9 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 10/22] arm64: Enable kpkeys Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 12/22] mm: kpkeys: Protect regular page tables Kevin Brodsky
` (10 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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 follows.
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. It also depends on
ARCH_HAS_SET_DIRECT_MAP to modify the direct mapping of page tables
using set_direct_map_pkey().
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).
Because this feature relies on kpkeys being supported and modifies
attributes of the linear map, it must be inactive during early boot.
mm_core_init() enables it before any call to pagetable_alloc() is
made.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
include/linux/kpkeys.h | 33 +++++++++++++++++++++++++++++++++
include/linux/kpkeys_types.h | 5 +++++
mm/Kconfig | 3 +++
mm/Makefile | 1 +
mm/kpkeys_hardened_pgtables.c | 16 ++++++++++++++++
mm/mm_init.c | 2 ++
security/Kconfig.hardening | 12 ++++++++++++
7 files changed, 72 insertions(+)
diff --git a/include/linux/kpkeys.h b/include/linux/kpkeys.h
index 69ec4808d850..cd3281223778 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -4,6 +4,7 @@
#include <linux/bug.h>
#include <linux/cleanup.h>
+#include <linux/jump_label.h>
#include <linux/kpkeys_types.h>
/**
@@ -104,4 +105,36 @@ static inline bool kpkeys_supported(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();
+}
+
+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/include/linux/kpkeys_types.h b/include/linux/kpkeys_types.h
index 9de59092e9c3..09f15a9565cf 100644
--- a/include/linux/kpkeys_types.h
+++ b/include/linux/kpkeys_types.h
@@ -8,10 +8,15 @@
#include <asm/kpkeys_types.h>
#endif
+#ifndef KPKEYS_PKEY_PGTABLES
+#define KPKEYS_PKEY_PGTABLES 1
+#endif
+
#if defined(CONFIG_ARCH_HAS_KPKEYS) && !defined(__ASSEMBLY__)
enum kpkeys_ctx {
KPKEYS_CTX_DEFAULT = 0,
+ KPKEYS_CTX_PGTABLES,
KPKEYS_CTX_COUNT,
};
diff --git a/mm/Kconfig b/mm/Kconfig
index 4635628de622..cf3b6105c4ad 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1267,6 +1267,9 @@ config ARCH_HAS_PKEYS
bool
config ARCH_HAS_KPKEYS
bool
+config ARCH_HAS_KPKEYS_HARDENED_PGTABLES
+ bool
+ depends on ARCH_HAS_KPKEYS && ARCH_HAS_SET_DIRECT_MAP
config ARCH_USES_PG_ARCH_2
bool
diff --git a/mm/Makefile b/mm/Makefile
index 2a3ec53d62ee..07b67eaa1b3b 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -149,3 +149,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_MEM_ALLOC_PROFILING) += alloc_tag.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..4c654e0cf585
--- /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_supported())
+ return;
+
+ static_branch_enable(&kpkeys_hardened_pgtables_key);
+}
diff --git a/mm/mm_init.c b/mm/mm_init.c
index d99af80ec752..347246135ce1 100644
--- a/mm/mm_init.c
+++ b/mm/mm_init.c
@@ -21,6 +21,7 @@
#include <linux/buffer_head.h>
#include <linux/kmemleak.h>
#include <linux/kfence.h>
+#include <linux/kpkeys.h>
#include <linux/page_ext.h>
#include <linux/pti.h>
#include <linux/pgtable.h>
@@ -2733,6 +2734,7 @@ void __init mm_core_init(void)
kmemleak_init();
ptlock_cache_init();
pgtable_cache_init();
+ kpkeys_hardened_pgtables_init();
debug_objects_mem_init();
vmalloc_init();
/* If no deferred init page_ext now, as vmap is fully initialized */
diff --git a/security/Kconfig.hardening b/security/Kconfig.hardening
index 81c81ad983ad..9a6f50103482 100644
--- a/security/Kconfig.hardening
+++ b/security/Kconfig.hardening
@@ -271,6 +271,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 12/22] mm: kpkeys: Protect regular page tables
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (10 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 11/22] mm: kpkeys: Introduce kpkeys_hardened_pgtables feature Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 13/22] mm: kpkeys: Protect early kernel " Kevin Brodsky
` (9 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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_direct_map_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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 cd3281223778..6c183c86b7d2 100644
--- a/include/linux/kpkeys.h
+++ b/include/linux/kpkeys.h
@@ -119,6 +119,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);
+
void kpkeys_hardened_pgtables_init(void);
#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
@@ -133,6 +136,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 6af3b7008f1a..2af713841035 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -39,6 +39,7 @@
#include <linux/iommu-debug-pagealloc.h>
#include <linux/kcsan-checks.h>
#include <linux/vmemmap-optimization.h>
+#include <linux/kpkeys.h>
struct mempolicy;
struct anon_vma;
@@ -3950,7 +3951,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);
}
@@ -3959,8 +3965,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 4c654e0cf585..2311fd8da6bb 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_direct_map_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_direct_map_pkey(addr, nr_pages, 0);
+
+ 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_supported())
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 13/22] mm: kpkeys: Protect early kernel page tables
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (11 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 12/22] mm: kpkeys: Protect regular page tables Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 14/22] mm: kpkeys: Protect vmemmap " Kevin Brodsky
` (8 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Initial kernel page tables (kernel image, direct map, vmemmap, etc.)
are not allocated using pagetable_alloc() - the buddy allocator
isn't ready at that point. We need to use a different approach to
protect them.
This patch takes the "brute force" route: when
kpkeys_hardened_pgtables is enabled, walk all installed kernel
page table, setting the pkey of their backing pages in the direct
map along the way.
The standard pagewalk API is used for that purpose, albeit in a
slightly unusual way:
* Visiting an entry is used to set the pkey of the table it points
to, hence: .pmd_entry -> protect_pte_table
* PTE entries are always leaves, so no need to visit them (no
.pte_entry callback)
* The range is meant to capture all installed kernel page tables.
There doesn't seem to be a generic way to express this;
[PAGE_OFFSET, ULONG_MAX) seems to be good enough on arm64/x86.
Locking probably isn't necessary so early during boot, but taking
the mmap_write_lock shouldn't hurt either. We also assume that all
page tables installed after that point are allocated using
pagetable_alloc(), which seems to be the case, at least on arm64.
There are some more assumptions - see comment above
protect_early_kernel_pgtables().
This simple approach should reliably protect all early kernel page
tables without invasive changes. However, adding support for large
blocks (PMD/PUD) is likely to be difficult and/or inefficient as
splitting will ocur when protecting individual page table pages.
Modifying the early page table allocators directly may be
preferable.
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
mm/kpkeys_hardened_pgtables.c | 122 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 122 insertions(+)
diff --git a/mm/kpkeys_hardened_pgtables.c b/mm/kpkeys_hardened_pgtables.c
index 2311fd8da6bb..f6825a0e0689 100644
--- a/mm/kpkeys_hardened_pgtables.c
+++ b/mm/kpkeys_hardened_pgtables.c
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/kpkeys.h>
#include <linux/mm.h>
+#include <linux/pagewalk.h>
+#include <linux/pgtable.h>
#include <linux/set_memory.h>
#include <kunit/visibility.h>
@@ -54,10 +56,130 @@ void kpkeys_pgtable_free(struct page *page, unsigned int order)
__free_pages(page, order);
}
+static void __init protect_pgtable_page(void *page)
+{
+ int ret;
+
+ ret = set_direct_map_pkey((unsigned long)page, 1, KPKEYS_PKEY_PGTABLES);
+ WARN_ON_ONCE(ret);
+}
+
+static int __init protect_pte_table(pmd_t *pmd, unsigned long addr,
+ unsigned long next, struct mm_walk *walk)
+{
+ pmd_t entry = pmdp_get(pmd);
+
+ if (!pmd_present(entry) || pmd_leaf(entry))
+ return 0;
+
+ protect_pgtable_page(pte_offset_kernel(pmd, 0));
+
+ return 0;
+}
+
+static int __init protect_pmd_table(pud_t *pud, unsigned long addr,
+ unsigned long next, struct mm_walk *walk)
+{
+ pud_t entry;
+
+ if (mm_pmd_folded(walk->mm))
+ return 0;
+
+ entry = pudp_get(pud);
+ if (!pud_present(entry) || pud_leaf(entry))
+ return 0;
+
+ protect_pgtable_page(pmd_offset(pud, 0));
+
+ return 0;
+}
+
+static int __init protect_pud_table(p4d_t *p4d, unsigned long addr,
+ unsigned long next, struct mm_walk *walk)
+{
+ p4d_t entry;
+
+ if (mm_pud_folded(walk->mm))
+ return 0;
+
+ entry = p4dp_get(p4d);
+ if (!p4d_present(entry) || p4d_leaf(entry))
+ return 0;
+
+ protect_pgtable_page(pud_offset(p4d, 0));
+
+ return 0;
+}
+
+static int __init protect_p4d_table(pgd_t *pgd, unsigned long addr,
+ unsigned long next, struct mm_walk *walk)
+{
+ pgd_t entry;
+
+ if (mm_p4d_folded(walk->mm))
+ return 0;
+
+ entry = pgdp_get(pgd);
+ if (!pgd_present(entry) || pgd_leaf(entry))
+ return 0;
+
+ protect_pgtable_page(p4d_offset(pgd, 0));
+
+ return 0;
+}
+
+static const struct mm_walk_ops protect_pgtable_ops __initconst = {
+ .pmd_entry = protect_pte_table,
+ .pud_entry = protect_pmd_table,
+ .p4d_entry = protect_pud_table,
+ .pgd_entry = protect_p4d_table,
+};
+
+/*
+ * Protect all installed kernel page tables by walking them and setting the
+ * direct mapping of all backing pages to pkey KPKEYS_PKEY_PGTABLES. We assume
+ * all non-root levels (PTE to P4D) are page-sized (asserts below).
+ *
+ * The root PGD page is left untouched; it is normally accessed via the kimg
+ * mapping only. On arm64 it cannot be written directly via any mapping.
+ *
+ * This approach is unsafe if set_direct_map_pkey() may split blocks in the
+ * direct map; we rely on the direct map being fully PTE-mapped.
+ */
+static void __init protect_early_kernel_pgtables(void)
+{
+ int ret;
+
+ mmap_write_lock(&init_mm);
+ /*
+ * [PAGE_OFFSET, ULONG_MAX) covers all early kernel mappings on arm64
+ * and x86 (at least).
+ */
+ ret = walk_kernel_page_table_range(PAGE_OFFSET, ULONG_MAX,
+ &protect_pgtable_ops, NULL, NULL);
+ mmap_write_unlock(&init_mm);
+ WARN_ON_ONCE(ret);
+}
+
+static_assert(MAX_PTRS_PER_PTE * sizeof(pte_t) == PAGE_SIZE);
+
+#ifndef __PAGETABLE_PMD_FOLDED
+static_assert(MAX_PTRS_PER_PMD * sizeof(pmd_t) == PAGE_SIZE);
+#endif
+
+#ifndef __PAGETABLE_PUD_FOLDED
+static_assert(MAX_PTRS_PER_PUD * sizeof(pud_t) == PAGE_SIZE);
+#endif
+
+#ifndef __PAGETABLE_P4D_FOLDED
+static_assert(MAX_PTRS_PER_P4D * sizeof(p4d_t) == PAGE_SIZE);
+#endif
+
void __init kpkeys_hardened_pgtables_init(void)
{
if (!kpkeys_supported())
return;
static_branch_enable(&kpkeys_hardened_pgtables_key);
+ protect_early_kernel_pgtables();
}
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 14/22] mm: kpkeys: Protect vmemmap page tables
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (12 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 13/22] mm: kpkeys: Protect early kernel " Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 15/22] arm64: kpkeys: Implement arch_supports_kpkeys_early() Kevin Brodsky
` (7 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
The kpkeys_hardened_pgtables feature automatically protect early
vmemmap page tables (covering standard boot memory) when it is
enabled.
In order to protect late vmemmap page tables as well, allocate them
like regular page tables using pagetable_alloc().
pagetable_alloc() does the right thing to protect the backing pages
using kpkeys, but isn't NUMA-aware, so the pages may be allocated on
any node. This could potentially incur some overhead on large NUMA
systems that make heavy use of memory hotplug.
The arm64 hotplug code is also amended to use a matching
pagetable_free(), ensuring that the pkey is reset when the page
tables are freed. x86 already uses pagetable_free() on that path.
Unlike in vmemmap_alloc_block(), __GFP_RETRY_MAYFAIL is not used as
it isn't justified for allocating page tables - this disables the
OOM and we do not have a fallback if we fail to allocate page
tables. See previous discussion linked below.
Link: https://lore.kernel.org/all/38d2a358-4146-bfc9-2a4f-68ce02f75c94@suse.cz/
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
* Moving the arm64 change to a separate commit?
* 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(). I sent a separate series [1] that partially
addresses this, but more work is still needed.
* x86 allows boot memory to be hot-removed in some circumstances.
free_pagetable() should handle this situation by resetting the
direct map pkey of reserved pages.
[1] https://lore.kernel.org/all/20260714-remove_pgtable_cdtor-v1-0-44be8a7685d7@arm.com/
---
arch/arm64/mm/mmu.c | 2 +-
mm/sparse-vmemmap.c | 23 ++++++++++++++++-------
2 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index 6a2155db3d05..a61ea65bd496 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -1496,7 +1496,7 @@ static void free_hotplug_page_range(struct page *page, size_t size,
static void free_hotplug_pgtable_page(struct page *page)
{
pagetable_dtor(page_ptdesc(page));
- free_hotplug_page_range(page, PAGE_SIZE, NULL);
+ pagetable_free(page_ptdesc(page));
}
static bool pgtable_range_aligned(unsigned long start, unsigned long end,
diff --git a/mm/sparse-vmemmap.c b/mm/sparse-vmemmap.c
index 5d7e982f94d0..c47d3a016891 100644
--- a/mm/sparse-vmemmap.c
+++ b/mm/sparse-vmemmap.c
@@ -150,13 +150,22 @@ int __meminit section_nr_vmemmap_pages(unsigned long pfn, unsigned long nr_pages
return 0;
}
-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()) {
+ gfp_t gfp = GFP_KERNEL | __GFP_ZERO;
+ struct ptdesc *ptdesc = pagetable_alloc(gfp, 0);
+
+ return ptdesc ? ptdesc_address(ptdesc) : NULL;
+ }
+
+ p = vmemmap_alloc_block(PAGE_SIZE, node);
if (!p)
return NULL;
- memset(p, 0, size);
+ memset(p, 0, PAGE_SIZE);
return p;
}
@@ -305,7 +314,7 @@ static pmd_t * __meminit vmemmap_pmd_populate(pud_t *pud, unsigned long addr, in
{
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);
@@ -318,7 +327,7 @@ static pud_t * __meminit vmemmap_pud_populate(p4d_t *p4d, unsigned long addr, in
{
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);
@@ -331,7 +340,7 @@ static p4d_t * __meminit vmemmap_p4d_populate(pgd_t *pgd, unsigned long addr, in
{
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);
@@ -344,7 +353,7 @@ static 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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 15/22] arm64: kpkeys: Implement arch_supports_kpkeys_early()
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (13 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 14/22] mm: kpkeys: Protect vmemmap " Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 16/22] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES Kevin Brodsky
` (6 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 e3c8a1493dc8..062d444da4ea 100644
--- a/arch/arm64/include/asm/cpufeature.h
+++ b/arch/arm64/include/asm/cpufeature.h
@@ -1114,6 +1114,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 4f9bdb369b47..70d85b0d69d6 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, enum kpkeys_ctx ctx)
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 16/22] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (14 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 15/22] arm64: kpkeys: Implement arch_supports_kpkeys_early() Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 17/22] arm64: kpkeys: Ensure the linear map can be modified Kevin Brodsky
` (5 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Enable RW access to KPKEYS_PKEY_PGTABLES (used to map page table
pages) if switching to KPKEYS_CTX_PGTABLES, otherwise only grant RO
access.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 70d85b0d69d6..6f6022b6cdcc 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(0, POE_RWX)
+#define POR_EL1_INIT (POR_ELx_PERM_PREP(0, 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, enum kpkeys_ctx ctx)
{
por = por_elx_set_pkey_perms(por, 0, POE_RWX);
+ por = por_elx_set_pkey_perms(por, KPKEYS_PKEY_PGTABLES,
+ ctx == KPKEYS_CTX_PGTABLES ? POE_RW : POE_R);
return por;
}
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 17/22] arm64: kpkeys: Ensure the linear map can be modified
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (15 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 16/22] arm64: kpkeys: Support KPKEYS_CTX_PGTABLES Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 18/22] arm64: kpkeys: Guard page table writes Kevin Brodsky
` (4 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
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 a61ea65bd496..c1e3260bd0db 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -781,7 +781,7 @@ static inline bool force_pte_mapping(void)
const bool bbml3 = system_capabilities_finalized() ?
system_supports_bbml3() : cpu_supports_bbml3();
- if (debug_pagealloc_enabled())
+ if (debug_pagealloc_enabled() || kpkeys_hardened_pgtables_early_enabled())
return true;
if (bbml3)
return false;
diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index 7143bb2dfecd..35f3e8ab0baa 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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 18/22] arm64: kpkeys: Guard page table writes
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (16 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 17/22] arm64: kpkeys: Ensure the linear map can be modified Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 19/22] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches Kevin Brodsky
` (3 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
When CONFIG_KPKEYS_HARDENED_PGTABLES is enabled, page tables (both
user and kernel) are mapped with a privileged pkey in the linear
mapping. As a result, they can only be written in a privileged
kpkeys context.
Introduce a kpkeys guard that sets POR_EL1 appropriately to allow
writing to page tables, and use this guard wherever necessary. The
scope is kept as small as possible, so that POR_EL1 is quickly reset
to its default value. Where atomics are involved, the guard's scope
encompasses the whole loop to avoid switching POR_EL1 unnecessarily.
This patch is a no-op if CONFIG_KPKEYS_HARDENED_PGTABLES is disabled
(default).
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable.h | 23 ++++++++++++++++++++++-
arch/arm64/mm/fault.c | 17 ++++++++++-------
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index a474c9fb485a..5f46d4cb3882 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -39,6 +39,14 @@
#include <linux/mm_types.h>
#include <linux/sched.h>
#include <linux/page_table_check.h>
+#include <linux/kpkeys.h>
+
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+KPKEYS_GUARD_COND(kpkeys_hardened_pgtables, KPKEYS_CTX_PGTABLES,
+ kpkeys_hardened_pgtables_enabled())
+#else
+KPKEYS_GUARD_NOOP(kpkeys_hardened_pgtables)
+#endif
static inline void emit_pte_barriers(void)
{
@@ -360,6 +368,7 @@ static inline pte_t pte_clear_uffd(pte_t pte)
static inline void __set_pte_nosync(pte_t *ptep, pte_t pte)
{
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*ptep, pte);
}
@@ -789,6 +798,7 @@ static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
}
#endif /* __PAGETABLE_PMD_FOLDED */
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pmdp, pmd);
if (pmd_valid(pmd))
@@ -850,6 +860,7 @@ static inline void set_pud(pud_t *pudp, pud_t pud)
return;
}
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pudp, pud);
if (pud_valid(pud))
@@ -928,6 +939,7 @@ static inline void set_p4d(p4d_t *p4dp, p4d_t p4d)
return;
}
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*p4dp, p4d);
queue_pte_barriers();
}
@@ -1053,6 +1065,7 @@ static inline void set_pgd(pgd_t *pgdp, pgd_t pgd)
return;
}
+ guard(kpkeys_hardened_pgtables)();
WRITE_ONCE(*pgdp, pgd);
queue_pte_barriers();
}
@@ -1254,6 +1267,7 @@ static inline bool __ptep_test_and_clear_young(struct vm_area_struct *vma,
{
pte_t old_pte, pte;
+ guard(kpkeys_hardened_pgtables)();
pte = __ptep_get(ptep);
do {
old_pte = pte;
@@ -1301,7 +1315,10 @@ static inline pte_t __ptep_get_and_clear_anysz(struct mm_struct *mm,
pte_t *ptep,
unsigned long pgsize)
{
- pte_t pte = __pte(xchg_relaxed(&pte_val(*ptep), 0));
+ pte_t pte;
+
+ scoped_guard(kpkeys_hardened_pgtables)
+ pte = __pte(xchg_relaxed(&pte_val(*ptep), 0));
switch (pgsize) {
case PAGE_SIZE:
@@ -1374,6 +1391,7 @@ static inline void ___ptep_set_wrprotect(struct mm_struct *mm,
{
pte_t old_pte;
+ guard(kpkeys_hardened_pgtables)();
do {
old_pte = pte;
pte = pte_wrprotect(pte);
@@ -1407,6 +1425,7 @@ static inline void __clear_young_dirty_pte(struct vm_area_struct *vma,
{
pte_t old_pte;
+ guard(kpkeys_hardened_pgtables)();
do {
old_pte = pte;
@@ -1454,6 +1473,7 @@ static inline pmd_t pmdp_establish(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmdp, pmd_t pmd)
{
page_table_check_pmd_set(vma->vm_mm, address, pmdp, pmd);
+ guard(kpkeys_hardened_pgtables)();
return __pmd(xchg_relaxed(&pmd_val(*pmdp), pmd_val(pmd)));
}
#endif
@@ -1784,6 +1804,7 @@ static inline bool ptep_try_set(pte_t *ptep, pte_t new_pte)
{
pteval_t old = 0;
+ guard(kpkeys_hardened_pgtables)();
if (!try_cmpxchg(&pte_val(*ptep), &old, pte_val(new_pte)))
return false;
diff --git a/arch/arm64/mm/fault.c b/arch/arm64/mm/fault.c
index b22736853fcd..37a4d8187aa1 100644
--- a/arch/arm64/mm/fault.c
+++ b/arch/arm64/mm/fault.c
@@ -239,13 +239,16 @@ int __ptep_set_access_flags_anysz(struct vm_area_struct *vma,
*/
pte_val(entry) ^= PTE_RDONLY;
pteval = pte_val(pte);
- do {
- old_pteval = pteval;
- pteval ^= PTE_RDONLY;
- pteval |= pte_val(entry);
- pteval ^= PTE_RDONLY;
- pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
- } while (pteval != old_pteval);
+ scoped_guard(kpkeys_hardened_pgtables) {
+ do {
+ old_pteval = pteval;
+ pteval ^= PTE_RDONLY;
+ pteval |= pte_val(entry);
+ pteval ^= PTE_RDONLY;
+ pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval,
+ pteval);
+ } while (pteval != old_pteval);
+ }
/*
* Invalidate the local stale read-only entry. Remote stale entries
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 19/22] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (17 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 18/22] arm64: kpkeys: Guard page table writes Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 20/22] arm64: kpkeys: Enable kpkeys_hardened_pgtables support Kevin Brodsky
` (2 subsequent siblings)
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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 struct
kpkeys_state somewhere. POR_EL1 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 state
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 lazy MMU 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.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/pgtable.h | 50 +++++++++++++++++++++++++++++++++++---
arch/arm64/include/asm/processor.h | 4 +++
2 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index 5f46d4cb3882..5e958531e6ab 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 inline void kpkeys_lazy_mmu_enter(void)
+{
+ if (!kpkeys_hardened_pgtables_enabled())
+ return;
+
+ current->thread.kpkeys_state_lazy_mmu =
+ kpkeys_enter_context(KPKEYS_CTX_PGTABLES);
+}
+
+static inline void kpkeys_lazy_mmu_exit(void)
+{
+ const struct kpkeys_state *state =
+ ¤t->thread.kpkeys_state_lazy_mmu;
+
+ if (!kpkeys_hardened_pgtables_enabled())
+ return;
+
+ /*
+ * 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) && state->entered_context)
+ __kpkeys_set_pkey_reg_nosync(state->arch_state.por);
+ else
+ kpkeys_leave_context(state);
+}
+#else /* CONFIG_KPKEYS_HARDENED_PGTABLES */
KPKEYS_GUARD_NOOP(kpkeys_hardened_pgtables)
-#endif
+
+static inline void kpkeys_lazy_mmu_enter(void) {}
+static inline 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 18424d91d8b2..e96c4d57d67d 100644
--- a/arch/arm64/include/asm/processor.h
+++ b/arch/arm64/include/asm/processor.h
@@ -30,6 +30,7 @@
#include <linux/build_bug.h>
#include <linux/cache.h>
#include <linux/init.h>
+#include <linux/kpkeys_types.h>
#include <linux/stddef.h>
#include <linux/string.h>
#include <linux/thread_info.h>
@@ -196,6 +197,9 @@ struct thread_struct {
u64 tpidr2_el0;
u64 por_el0;
u64 por_el1;
+#ifdef CONFIG_KPKEYS_HARDENED_PGTABLES
+ struct kpkeys_state kpkeys_state_lazy_mmu;
+#endif
#ifdef CONFIG_ARM64_GCS
unsigned int gcs_el0_mode;
unsigned int gcs_el0_locked;
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 20/22] arm64: kpkeys: Enable kpkeys_hardened_pgtables support
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (18 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 19/22] arm64: kpkeys: Batch KPKEYS_CTX_PGTABLES switches Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 21/22] arm64: mm: Map kernel image alias of init_pg_dir read-only Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 22/22] mm: Add basic tests for kpkeys_hardened_pgtables Kevin Brodsky
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
All the pieces are in place to advertise support for
kpkeys_hardened_pgtables. Do it 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 +
1 file changed, 1 insertion(+)
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index f7e4ffbc07ac..28023dd0fd4b 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -2301,6 +2301,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
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 21/22] arm64: mm: Map kernel image alias of init_pg_dir read-only
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (19 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 20/22] arm64: kpkeys: Enable kpkeys_hardened_pgtables support Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 15:58 ` [PATCH RFC v10 22/22] mm: Add basic tests for kpkeys_hardened_pgtables Kevin Brodsky
21 siblings, 0 replies; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
Page tables in init_pg_dir are always modified via the linear map.
kpkeys_hardened_pgtables now prevents arbitrary code from writing
page tables via the linear map; map the kernel image alias of
init_pg_dir read-only to ensure that the kpkeys protection cannot be
bypassed.
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
arch/arm64/include/asm/sections.h | 1 +
arch/arm64/kernel/pi/map_kernel.c | 9 +++++++--
arch/arm64/mm/mmu.c | 8 ++++++++
3 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/arch/arm64/include/asm/sections.h b/arch/arm64/include/asm/sections.h
index 51b0d594239e..dc961cf67168 100644
--- a/arch/arm64/include/asm/sections.h
+++ b/arch/arm64/include/asm/sections.h
@@ -23,6 +23,7 @@ extern char __irqentry_text_start[], __irqentry_text_end[];
extern char __mmuoff_data_start[], __mmuoff_data_end[];
extern char __entry_tramp_text_start[], __entry_tramp_text_end[];
extern char __relocate_new_kernel_start[], __relocate_new_kernel_end[];
+extern char __pi_init_pg_dir[], __pi_init_pg_end[];
static inline size_t entry_tramp_text_size(void)
{
diff --git a/arch/arm64/kernel/pi/map_kernel.c b/arch/arm64/kernel/pi/map_kernel.c
index fb44cbdd2f29..81ca3e98f0f7 100644
--- a/arch/arm64/kernel/pi/map_kernel.c
+++ b/arch/arm64/kernel/pi/map_kernel.c
@@ -92,8 +92,13 @@ static void __init map_kernel(u64 kaslr_offset, u64 va_offset, int root_level)
__inittext_end, prot, false, root_level);
map_segment(init_pg_dir, &pgdp, va_offset, __initdata_begin,
__initdata_end, data_prot, false, root_level);
- map_segment(init_pg_dir, &pgdp, va_offset, _data, _end, data_prot,
- true, root_level);
+ map_segment(init_pg_dir, &pgdp, va_offset, _data, init_pg_dir,
+ data_prot, true, root_level);
+ /* Map init_pg_dir separately as we will remap it read-only later on */
+ map_segment(init_pg_dir, &pgdp, va_offset, init_pg_dir, init_pg_end,
+ data_prot, true, root_level);
+ map_segment(init_pg_dir, &pgdp, va_offset, init_pg_end, _end,
+ data_prot, true, root_level);
dsb(ishst);
idmap_cpu_replace_ttbr1((phys_addr_t)init_pg_dir);
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index c1e3260bd0db..b6a4371dd03f 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -1267,6 +1267,14 @@ void mark_rodata_ro(void)
update_mapping_prot(__pa_symbol(_text), (unsigned long)_text,
(unsigned long)_stext - (unsigned long)_text,
PAGE_KERNEL_RO);
+ /*
+ * Map the kernel image mapping of init_pg_dir read-only; it should
+ * only be written via the linear map.
+ */
+ section_size = (unsigned long)__pi_init_pg_end - (unsigned long)__pi_init_pg_dir;
+ update_mapping_prot(__pa_symbol(__pi_init_pg_dir),
+ (unsigned long)__pi_init_pg_dir,
+ section_size, PAGE_KERNEL_RO);
/* Map the kernel data/bss as invalid in the linear map */
mark_linear_data_alias_valid(false);
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* [PATCH RFC v10 22/22] mm: Add basic tests for kpkeys_hardened_pgtables
2026-11-09 15:58 [PATCH RFC v10 00/22] pkeys-based page table hardening Kevin Brodsky
` (20 preceding siblings ...)
2026-11-09 15:58 ` [PATCH RFC v10 21/22] arm64: mm: Map kernel image alias of init_pg_dir read-only Kevin Brodsky
@ 2026-11-09 15:58 ` Kevin Brodsky
2026-11-09 19:28 ` Mark Brown
21 siblings, 1 reply; 24+ messages in thread
From: Kevin Brodsky @ 2026-11-09 15:58 UTC (permalink / raw)
To: linux-hardening
Cc: Kevin Brodsky, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Mark Brown, Matthew Wilcox,
Maxwell Bland, Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
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()).
Reviewed-by: Yeoreum Yun <yeoreum.yun@arm.com>
Signed-off-by: Kevin Brodsky <kevin.brodsky@arm.com>
---
mm/Makefile | 1 +
mm/tests/kpkeys_hardened_pgtables_kunit.c | 200 ++++++++++++++++++++++++++++++
security/Kconfig.hardening | 12 ++
3 files changed, 213 insertions(+)
diff --git a/mm/Makefile b/mm/Makefile
index 07b67eaa1b3b..4acdd3c58b46 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -150,3 +150,4 @@ obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o
obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o
obj-$(CONFIG_MEM_ALLOC_PROFILING) += alloc_tag.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..b6e1eba0d9b9
--- /dev/null
+++ b/mm/tests/kpkeys_hardened_pgtables_kunit.c
@@ -0,0 +1,200 @@
+// 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_supported())
+ 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, 1);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = set_direct_map_default_noflush(page, 1);
+ 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");
+ if (!mm_p4d_folded(&init_mm))
+ write_pgtable(p4d, p4dp);
+
+ pudp = pud_offset(p4dp, addr);
+ KUNIT_ASSERT_NOT_NULL_MSG(test, pudp, "Failed to get PUD");
+ if (!mm_pud_folded(&init_mm))
+ 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_supported())
+ 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_supported())
+ 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_supported())
+ 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_supported())
+ 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 9a6f50103482..af0f83e3a19c 100644
--- a/security/Kconfig.hardening
+++ b/security/Kconfig.hardening
@@ -283,6 +283,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.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread* Re: [PATCH RFC v10 22/22] mm: Add basic tests for kpkeys_hardened_pgtables
2026-11-09 15:58 ` [PATCH RFC v10 22/22] mm: Add basic tests for kpkeys_hardened_pgtables Kevin Brodsky
@ 2026-11-09 19:28 ` Mark Brown
0 siblings, 0 replies; 24+ messages in thread
From: Mark Brown @ 2026-11-09 19:28 UTC (permalink / raw)
To: Kevin Brodsky
Cc: linux-hardening, Andrew Morton, Ard Biesheuvel, Andy Lutomirski,
Catalin Marinas, Dave Hansen, David Hildenbrand (Arm),
Jann Horn, Jeff Xu, Joey Gouly, Kees Cook, Linu Cherian,
Linus Walleij, Marc Zyngier, Matthew Wilcox, Maxwell Bland,
Mike Rapoport (IBM),
Peter Zijlstra, Pierre Langlois, Pierre-Clément Tosi,
Quentin Perret, Rick Edgecombe, Ryan Roberts, Vlastimil Babka,
Will Deacon, Yang Shi, Yeoreum Yun, linux-kernel,
linux-arm-kernel, linux-mm, x86, Ira Weiny, Lorenzo Stoakes,
Thomas Gleixner
[-- Attachment #1: Type: text/plain, Size: 713 bytes --]
On Mon, Nov 09, 2026 at 03:58:47PM +0000, Kevin Brodsky wrote:
> ---
> mm/Makefile | 1 +
> mm/tests/kpkeys_hardened_pgtables_kunit.c | 200 ++++++++++++++++++++++++++++++
> security/Kconfig.hardening | 12 ++
> 3 files changed, 213 insertions(+)
> +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
I'd also expect to see this added to one of the KUnit config fragments
somewhere so it gets run as standard, though that's a little awkward
given that KPKEYS_HARDENED_PGTABLES isn't universally available.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply [flat|nested] 24+ messages in thread