* [RFC PATCH v3 1/3] mm: make persistent huge zero folio read-only
2026-07-06 13:04 [RFC PATCH v3 0/3] make persistent huge zero folio read-only Xueyuan Chen
@ 2026-07-06 13:04 ` Xueyuan Chen
2026-07-07 13:17 ` Usama Arif
2026-07-06 13:04 ` [RFC PATCH v3 2/3] arm64/mm: add set_direct_map_ro_noflush() Xueyuan Chen
2026-07-06 13:04 ` [RFC PATCH v3 3/3] x86/mm: " Xueyuan Chen
2 siblings, 1 reply; 5+ messages in thread
From: Xueyuan Chen @ 2026-07-06 13:04 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Catalin Marinas, Will Deacon,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
linux-mm, linux-arm-kernel, linux-kernel
Cc: Lance Yang, Jann Horn, Yang Shi, Mike Rapoport, Lorenzo Stoakes,
Zi Yan, Baolin Wang, Liam R . Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, H . Peter Anvin, Andy Lutomirski,
Peter Zijlstra, Xueyuan Chen
From: Xueyuan Chen <xueyuan.chen21@gmail.com>
The persistent huge zero folio is shared globally and should stay zero
after initialization. As Jann Horn pointed out[1], kernel bugs have ended
up writing to pages that were meant to be read-only, including in
security-sensitive cases. Making the persistent huge zero folio read-only
in the direct map turns such writes into faults instead of silent zero-page
corruption.
Add set_direct_map_ro_noflush() so mm code can make a direct-map range
read-only. Use an address-based signature to match ongoing direct-map
helper work[2], where existing page-based helpers may move the same way.
The helper is direct-map specific and does not flush TLBs. Architectures
without direct-map permission support keep existing behavior through the
generic stub.
Persistent huge zero folio setup happens during early boot, so no explicit
TLB flush is needed.
Inspired by Jann Horn's read-only zero page work[1] and follow-up
discussion[3] with Yang Shi.
[1] https://lore.kernel.org/linux-mm/20260508-ro-zeropage-v1-1-9808abc20b49@google.com/
[2] https://lore.kernel.org/linux-mm/0e5b23a6-4895-454a-9dfa-6dc21adc2991@kernel.org/
[3] https://lore.kernel.org/linux-mm/CAHbLzkrXXe7r3n3jXgDKtwZhRqj=jDx9E6dLOULohnhBguvi9A@mail.gmail.com/
Suggested-by: David Hildenbrand <david@kernel.org>
Co-developed-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Xueyuan Chen <xueyuan.chen21@gmail.com>
---
include/linux/set_memory.h | 26 ++++++++++++++++++++++++++
mm/huge_memory.c | 9 ++++++++-
2 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h
index 3030d9245f5a..a905074fb21d 100644
--- a/include/linux/set_memory.h
+++ b/include/linux/set_memory.h
@@ -40,6 +40,22 @@ static inline int set_direct_map_valid_noflush(struct page *page,
return 0;
}
+/**
+ * set_direct_map_ro_noflush - make direct-map mappings read-only
+ * @addr: start address in the direct map
+ * @nr_pages: number of pages starting at @addr
+ *
+ * Make the direct-map mappings for @nr_pages pages starting at @addr
+ * read-only, without flushing TLBs.
+ *
+ * Return: 0 on success or when unsupported, negative error code on failure.
+ */
+static inline int set_direct_map_ro_noflush(const void *addr,
+ unsigned long nr_pages)
+{
+ return 0;
+}
+
static inline bool kernel_page_present(struct page *page)
{
return true;
@@ -56,6 +72,16 @@ static inline bool can_set_direct_map(void)
}
#define can_set_direct_map can_set_direct_map
#endif
+
+#ifndef set_direct_map_ro_noflush
+static inline int set_direct_map_ro_noflush(const void *addr,
+ unsigned long nr_pages)
+{
+ return 0;
+}
+
+#define set_direct_map_ro_noflush set_direct_map_ro_noflush
+#endif
#endif /* CONFIG_ARCH_HAS_SET_DIRECT_MAP */
#ifdef CONFIG_X86_64
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index bdd8635922f9..6633217b10dc 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -41,6 +41,7 @@
#include <linux/pgalloc.h>
#include <linux/pgalloc_tag.h>
#include <linux/pagewalk.h>
+#include <linux/set_memory.h>
#include <asm/tlb.h>
#include "internal.h"
@@ -981,8 +982,14 @@ static int __init thp_shrinker_init(void)
* that get_huge_zero_folio() will most likely not fail as
* thp_shrinker_init() is invoked early on during boot.
*/
- if (!get_huge_zero_folio())
+ if (!get_huge_zero_folio()) {
pr_warn("Allocating persistent huge zero folio failed\n");
+ return 0;
+ }
+
+ /* Set up during early boot; no explicit TLB flush is needed here. */
+ set_direct_map_ro_noflush(folio_address(huge_zero_folio),
+ HPAGE_PMD_NR);
return 0;
}
--
2.49.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [RFC PATCH v3 1/3] mm: make persistent huge zero folio read-only
2026-07-06 13:04 ` [RFC PATCH v3 1/3] mm: " Xueyuan Chen
@ 2026-07-07 13:17 ` Usama Arif
0 siblings, 0 replies; 5+ messages in thread
From: Usama Arif @ 2026-07-07 13:17 UTC (permalink / raw)
To: Xueyuan Chen
Cc: Usama Arif, Andrew Morton, David Hildenbrand, Catalin Marinas,
Will Deacon, Thomas Gleixner, Ingo Molnar, Borislav Petkov,
Dave Hansen, x86, linux-mm, linux-arm-kernel, linux-kernel,
Lance Yang, Jann Horn, Yang Shi, Mike Rapoport, Lorenzo Stoakes,
Zi Yan, Baolin Wang, Liam R . Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, H . Peter Anvin, Andy Lutomirski,
Peter Zijlstra
On Mon, 6 Jul 2026 21:04:38 +0800 Xueyuan Chen <xueyuan.chen21@gmail.com> wrote:
> From: Xueyuan Chen <xueyuan.chen21@gmail.com>
>
> The persistent huge zero folio is shared globally and should stay zero
> after initialization. As Jann Horn pointed out[1], kernel bugs have ended
> up writing to pages that were meant to be read-only, including in
> security-sensitive cases. Making the persistent huge zero folio read-only
> in the direct map turns such writes into faults instead of silent zero-page
> corruption.
>
> Add set_direct_map_ro_noflush() so mm code can make a direct-map range
> read-only. Use an address-based signature to match ongoing direct-map
> helper work[2], where existing page-based helpers may move the same way.
> The helper is direct-map specific and does not flush TLBs. Architectures
> without direct-map permission support keep existing behavior through the
> generic stub.
>
> Persistent huge zero folio setup happens during early boot, so no explicit
> TLB flush is needed.
>
> Inspired by Jann Horn's read-only zero page work[1] and follow-up
> discussion[3] with Yang Shi.
>
> [1] https://lore.kernel.org/linux-mm/20260508-ro-zeropage-v1-1-9808abc20b49@google.com/
> [2] https://lore.kernel.org/linux-mm/0e5b23a6-4895-454a-9dfa-6dc21adc2991@kernel.org/
> [3] https://lore.kernel.org/linux-mm/CAHbLzkrXXe7r3n3jXgDKtwZhRqj=jDx9E6dLOULohnhBguvi9A@mail.gmail.com/
>
> Suggested-by: David Hildenbrand <david@kernel.org>
> Co-developed-by: Lance Yang <lance.yang@linux.dev>
> Signed-off-by: Lance Yang <lance.yang@linux.dev>
> Signed-off-by: Xueyuan Chen <xueyuan.chen21@gmail.com>
> ---
> include/linux/set_memory.h | 26 ++++++++++++++++++++++++++
> mm/huge_memory.c | 9 ++++++++-
> 2 files changed, 34 insertions(+), 1 deletion(-)
>
> diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h
> index 3030d9245f5a..a905074fb21d 100644
> --- a/include/linux/set_memory.h
> +++ b/include/linux/set_memory.h
> @@ -40,6 +40,22 @@ static inline int set_direct_map_valid_noflush(struct page *page,
> return 0;
> }
>
> +/**
> + * set_direct_map_ro_noflush - make direct-map mappings read-only
> + * @addr: start address in the direct map
> + * @nr_pages: number of pages starting at @addr
> + *
> + * Make the direct-map mappings for @nr_pages pages starting at @addr
> + * read-only, without flushing TLBs.
> + *
> + * Return: 0 on success or when unsupported, negative error code on failure.
> + */
> +static inline int set_direct_map_ro_noflush(const void *addr,
> + unsigned long nr_pages)
> +{
> + return 0;
> +}
> +
> static inline bool kernel_page_present(struct page *page)
> {
> return true;
> @@ -56,6 +72,16 @@ static inline bool can_set_direct_map(void)
> }
> #define can_set_direct_map can_set_direct_map
> #endif
> +
> +#ifndef set_direct_map_ro_noflush
> +static inline int set_direct_map_ro_noflush(const void *addr,
> + unsigned long nr_pages)
> +{
> + return 0;
> +}
> +
> +#define set_direct_map_ro_noflush set_direct_map_ro_noflush
> +#endif
> #endif /* CONFIG_ARCH_HAS_SET_DIRECT_MAP */
>
> #ifdef CONFIG_X86_64
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index bdd8635922f9..6633217b10dc 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -41,6 +41,7 @@
> #include <linux/pgalloc.h>
> #include <linux/pgalloc_tag.h>
> #include <linux/pagewalk.h>
> +#include <linux/set_memory.h>
>
> #include <asm/tlb.h>
> #include "internal.h"
> @@ -981,8 +982,14 @@ static int __init thp_shrinker_init(void)
> * that get_huge_zero_folio() will most likely not fail as
> * thp_shrinker_init() is invoked early on during boot.
> */
> - if (!get_huge_zero_folio())
> + if (!get_huge_zero_folio()) {
> pr_warn("Allocating persistent huge zero folio failed\n");
> + return 0;
> + }
> +
> + /* Set up during early boot; no explicit TLB flush is needed here. */
> + set_direct_map_ro_noflush(folio_address(huge_zero_folio),
> + HPAGE_PMD_NR);
Hi,
Can this really skip the TLB flush here? This runs from a
subsys_initcall, after smp_init() and after the folio was allocated and
zeroed through the writable direct map. The noflush helper updates the
direct-map PTEs but does not invalidate stale writable kernel TLB entries.
I think we need a TLB flush here?
> return 0;
> }
>
> --
> 2.49.0
>
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH v3 2/3] arm64/mm: add set_direct_map_ro_noflush()
2026-07-06 13:04 [RFC PATCH v3 0/3] make persistent huge zero folio read-only Xueyuan Chen
2026-07-06 13:04 ` [RFC PATCH v3 1/3] mm: " Xueyuan Chen
@ 2026-07-06 13:04 ` Xueyuan Chen
2026-07-06 13:04 ` [RFC PATCH v3 3/3] x86/mm: " Xueyuan Chen
2 siblings, 0 replies; 5+ messages in thread
From: Xueyuan Chen @ 2026-07-06 13:04 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Catalin Marinas, Will Deacon,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
linux-mm, linux-arm-kernel, linux-kernel
Cc: Lance Yang, Jann Horn, Yang Shi, Mike Rapoport, Lorenzo Stoakes,
Zi Yan, Baolin Wang, Liam R . Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, H . Peter Anvin, Andy Lutomirski,
Peter Zijlstra, Xueyuan Chen
From: Xueyuan Chen <xueyuan.chen21@gmail.com>
Implement set_direct_map_ro_noflush() for arm64 with update_range_prot() on
the linear map, setting PTE_RDONLY and clearing PTE_WRITE. Keep the
existing can_set_direct_map() guard and leave TLB invalidation to the
caller.
Co-developed-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Xueyuan Chen <xueyuan.chen21@gmail.com>
---
arch/arm64/include/asm/set_memory.h | 2 ++
arch/arm64/mm/pageattr.c | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/arch/arm64/include/asm/set_memory.h b/arch/arm64/include/asm/set_memory.h
index 90f61b17275e..7083260303c3 100644
--- a/arch/arm64/include/asm/set_memory.h
+++ b/arch/arm64/include/asm/set_memory.h
@@ -14,6 +14,8 @@ int set_memory_valid(unsigned long addr, int numpages, int enable);
int set_direct_map_invalid_noflush(struct page *page);
int set_direct_map_default_noflush(struct page *page);
int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid);
+int set_direct_map_ro_noflush(const void *addr, unsigned long nr_pages);
+#define set_direct_map_ro_noflush set_direct_map_ro_noflush
bool kernel_page_present(struct page *page);
int set_memory_encrypted(unsigned long addr, int numpages);
diff --git a/arch/arm64/mm/pageattr.c b/arch/arm64/mm/pageattr.c
index bbe98ac9ad8c..1a6769073558 100644
--- a/arch/arm64/mm/pageattr.c
+++ b/arch/arm64/mm/pageattr.c
@@ -365,6 +365,16 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid)
return set_memory_valid(addr, nr, valid);
}
+int set_direct_map_ro_noflush(const void *addr, unsigned long nr_pages)
+{
+ if (!can_set_direct_map())
+ return 0;
+
+ return update_range_prot((unsigned long)addr, PAGE_SIZE * nr_pages,
+ __pgprot(PTE_RDONLY),
+ __pgprot(PTE_WRITE));
+}
+
#ifdef CONFIG_DEBUG_PAGEALLOC
/*
* This is - apart from the return value - doing the same
--
2.49.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [RFC PATCH v3 3/3] x86/mm: add set_direct_map_ro_noflush()
2026-07-06 13:04 [RFC PATCH v3 0/3] make persistent huge zero folio read-only Xueyuan Chen
2026-07-06 13:04 ` [RFC PATCH v3 1/3] mm: " Xueyuan Chen
2026-07-06 13:04 ` [RFC PATCH v3 2/3] arm64/mm: add set_direct_map_ro_noflush() Xueyuan Chen
@ 2026-07-06 13:04 ` Xueyuan Chen
2 siblings, 0 replies; 5+ messages in thread
From: Xueyuan Chen @ 2026-07-06 13:04 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Catalin Marinas, Will Deacon,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
linux-mm, linux-arm-kernel, linux-kernel
Cc: Lance Yang, Jann Horn, Yang Shi, Mike Rapoport, Lorenzo Stoakes,
Zi Yan, Baolin Wang, Liam R . Howlett, Nico Pache, Ryan Roberts,
Dev Jain, Barry Song, H . Peter Anvin, Andy Lutomirski,
Peter Zijlstra, Xueyuan Chen
From: Xueyuan Chen <xueyuan.chen21@gmail.com>
Implement set_direct_map_ro_noflush() for x86 using CPA directly on the
passed direct-map address. Clear _PAGE_RW and _PAGE_DIRTY, keep alias
checks disabled like the existing direct-map _noflush helpers, and leave
TLB invalidation to the caller.
Co-developed-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Xueyuan Chen <xueyuan.chen21@gmail.com>
---
arch/x86/include/asm/set_memory.h | 2 ++
arch/x86/mm/pat/set_memory.c | 15 +++++++++++++++
2 files changed, 17 insertions(+)
diff --git a/arch/x86/include/asm/set_memory.h b/arch/x86/include/asm/set_memory.h
index 4362c26aa992..bd3817e06052 100644
--- a/arch/x86/include/asm/set_memory.h
+++ b/arch/x86/include/asm/set_memory.h
@@ -89,6 +89,8 @@ int set_pages_rw(struct page *page, int numpages);
int set_direct_map_invalid_noflush(struct page *page);
int set_direct_map_default_noflush(struct page *page);
int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid);
+int set_direct_map_ro_noflush(const void *addr, unsigned long nr_pages);
+#define set_direct_map_ro_noflush set_direct_map_ro_noflush
bool kernel_page_present(struct page *page);
extern int kernel_set_to_readonly;
diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
index d023a40a1e03..5987f4c84f6f 100644
--- a/arch/x86/mm/pat/set_memory.c
+++ b/arch/x86/mm/pat/set_memory.c
@@ -2662,6 +2662,21 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid)
return __set_pages_np(page, nr);
}
+int set_direct_map_ro_noflush(const void *addr, unsigned long nr_pages)
+{
+ unsigned long tempaddr = (unsigned long)addr;
+ struct cpa_data cpa = {
+ .vaddr = &tempaddr,
+ .pgd = NULL,
+ .numpages = nr_pages,
+ .mask_set = __pgprot(0),
+ .mask_clr = __pgprot(_PAGE_RW | _PAGE_DIRTY),
+ .flags = CPA_NO_CHECK_ALIAS,
+ };
+
+ return __change_page_attr_set_clr(&cpa, 1);
+}
+
#ifdef CONFIG_DEBUG_PAGEALLOC
void __kernel_map_pages(struct page *page, int numpages, int enable)
{
--
2.49.0
^ permalink raw reply related [flat|nested] 5+ messages in thread