[PATCH v3 07/26] x86/mm: introduce mm-local region
From: Brendan Jackman
Date: Sun Jul 26 2026 - 18:23:46 EST
Various security features benefit from having process-local address
mappings within the kernel. Examples include no-direct-map guest_memfd
[2] and significant optimizations for ASI [1].
With the currently envisaged usecases, there will be many situations
where almost no processes have any need for the mm-local region.
Therefore, avoid its overhead (memory cost of pagetables, alloc/free
overhead during fork/exit) for processes that don't use it by requiring
its users to explicitly initialize it via the new mm_local_* API.
As pointed out by Andy in [0], x86 already has a PGD entry that is local
to the mm, which is used for the LDT. In a subsequent patch, the LDT
remap will be unified with the general mm-local region, but to help
keep the patch to a manageable size, first just introduce the mm-local
region.
On 64-bit, give the mm-local region a whole PGD. On 32-bit, just give it
one PMD. No investigation has been done into whether it's feasible to
expand the region on 32-bit. Most likely there is no strong usecase for
that anyway.
In order to combine the need for an on-demand mm initialisation, with
the desire to transparently handle propagating mappings to userspace
under KPTI, the user and kernel pagetables are shared at the highest
level possible. For PAE that means the PTE table is shared and for
64-bit the P4D/PUD. This is implemented by pre-allocating the first
shared table when the mm-local region is first initialised.
[0] https://lore.kernel.org/linux-mm/CALCETrXHbS9VXfZ80kOjiTrreM2EbapYeGp68mvJPbosUtorYA@xxxxxxxxxxxxxx/
[1] https://linuxasi.dev/
[2] https://lore.kernel.org/all/20250924151101.2225820-1-patrick.roy@xxxxxxxxxxxxx
Signed-off-by: Brendan Jackman <jackmanb@xxxxxxxxxx>
---
arch/x86/Kconfig | 2 +
arch/x86/include/asm/mmu_context.h | 118 +++++++++++++++++++++++++++++++-
arch/x86/include/asm/pgtable_32_areas.h | 9 ++-
arch/x86/include/asm/pgtable_64_types.h | 12 +++-
arch/x86/mm/pgtable.c | 3 +
include/linux/mm.h | 13 ++++
include/linux/mm_types.h | 2 +
kernel/fork.c | 1 +
mm/Kconfig | 7 ++
9 files changed, 161 insertions(+), 6 deletions(-)
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index fb298e2191792..3efab3524a6cf 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -132,6 +132,8 @@ config X86
select ARCH_SUPPORTS_LTO_CLANG
select ARCH_SUPPORTS_LTO_CLANG_THIN
select ARCH_SUPPORTS_RT
+ # LDT remap temporarily clashes with mm-local region, can't have both.
+ select ARCH_SUPPORTS_MM_LOCAL_REGION if X86_64 || X86_PAE && !MODIFY_LDT_SYSCALL
select ARCH_USE_BUILTIN_BSWAP
select ARCH_USE_CMPXCHG_LOCKREF
select ARCH_USE_MEMTEST
diff --git a/arch/x86/include/asm/mmu_context.h b/arch/x86/include/asm/mmu_context.h
index ef5b507de34e2..3d4f54673014f 100644
--- a/arch/x86/include/asm/mmu_context.h
+++ b/arch/x86/include/asm/mmu_context.h
@@ -8,8 +8,10 @@
#include <trace/events/tlb.h>
+#include <asm/tlb.h>
#include <asm/tlbflush.h>
#include <asm/paravirt.h>
+#include <asm/pgalloc.h>
#include <asm/debugreg.h>
#include <asm/gsseg.h>
#include <asm/desc.h>
@@ -223,10 +225,124 @@ static inline int arch_dup_mmap(struct mm_struct *oldmm, struct mm_struct *mm)
return ldt_dup_context(oldmm, mm);
}
+#ifdef CONFIG_MM_LOCAL_REGION
+static inline void mm_local_region_free(struct mm_struct *mm)
+{
+ if (!mm_local_region_used(mm))
+ return;
+
+ struct mmu_gather tlb;
+ unsigned long start = MM_LOCAL_BASE_ADDR;
+ unsigned long end = MM_LOCAL_END_ADDR;
+
+ /*
+ * Although free_pgd_range() is intended for freeing user
+ * page-tables, it also works out for kernel mappings on x86.
+ * Use tlb_gather_mmu_fullmm() to avoid confusing the
+ * range-tracking logic in __tlb_adjust_range().
+ */
+ tlb_gather_mmu_fullmm(&tlb, mm);
+ free_pgd_range(&tlb, start, end, start, end);
+ tlb_finish_mmu(&tlb);
+
+ mm_flags_clear(MMF_LOCAL_REGION_USED, mm);
+}
+
+#if defined(CONFIG_MITIGATION_PAGE_TABLE_ISOLATION) && defined(CONFIG_X86_PAE)
+static inline pmd_t *pgd_to_pmd_walk(pgd_t *pgd, unsigned long va)
+{
+ p4d_t *p4d;
+ pud_t *pud;
+
+ if (pgd->pgd == 0)
+ return NULL;
+
+ p4d = p4d_offset(pgd, va);
+ if (p4d_none(*p4d))
+ return NULL;
+
+ pud = pud_offset(p4d, va);
+ if (pud_none(*pud))
+ return NULL;
+
+ return pmd_offset(pud, va);
+}
+
+static inline int mm_local_map_to_user(struct mm_struct *mm)
+{
+ BUILD_BUG_ON(!PREALLOCATED_PMDS);
+ pgd_t *k_pgd = pgd_offset(mm, MM_LOCAL_BASE_ADDR);
+ pgd_t *u_pgd = kernel_to_user_pgdp(k_pgd);
+ pmd_t *k_pmd, *u_pmd;
+ int err;
+
+ k_pmd = pgd_to_pmd_walk(k_pgd, MM_LOCAL_BASE_ADDR);
+ u_pmd = pgd_to_pmd_walk(u_pgd, MM_LOCAL_BASE_ADDR);
+
+ BUILD_BUG_ON(MM_LOCAL_END_ADDR - MM_LOCAL_BASE_ADDR > PMD_SIZE);
+
+ /* Preallocate the PTE table so it can be shared. */
+ err = pte_alloc(mm, k_pmd);
+ if (err)
+ return err;
+
+ /* Point the userspace PMD at the same PTE as the kernel PMD. */
+ set_pmd(u_pmd, *k_pmd);
+ return 0;
+}
+#elif defined(CONFIG_MITIGATION_PAGE_TABLE_ISOLATION)
+static inline int mm_local_map_to_user(struct mm_struct *mm)
+{
+ pgd_t *pgd;
+ int err;
+
+ err = preallocate_sub_pgd(mm, MM_LOCAL_BASE_ADDR);
+ if (err)
+ return err;
+
+ pgd = pgd_offset(mm, MM_LOCAL_BASE_ADDR);
+ set_pgd(kernel_to_user_pgdp(pgd), *pgd);
+ return 0;
+}
+#else
+static inline int mm_local_map_to_user(struct mm_struct *mm)
+{
+ WARN_ONCE(1, "mm_local_map_to_user() not implemented");
+ return -EINVAL;
+}
+#endif
+
+/*
+ * Do initial setup of the mm-local region. Call from process context.
+ *
+ * Under PTI, userspace shares the pagetables for the mm-local region with the
+ * kernel (if you map stuff here, it's immediately mapped into userspace too).
+ * LDT remap. It's assuming nothing gets mapped in here that needs to be
+ * protected from Meltdown-type attacks from the current process.
+ */
+static inline int mm_local_region_init(struct mm_struct *mm)
+{
+ int err;
+
+ if (boot_cpu_has(X86_FEATURE_PTI)) {
+ err = mm_local_map_to_user(mm);
+ if (err)
+ return err;
+ }
+
+ mm_flags_set(MMF_LOCAL_REGION_USED, mm);
+
+ return 0;
+}
+
+#else
+static inline void mm_local_region_free(struct mm_struct *mm) { }
+#endif /* CONFIG_MM_LOCAL_REGION */
+
static inline void arch_exit_mmap(struct mm_struct *mm)
{
paravirt_arch_exit_mmap(mm);
- ldt_arch_exit_mmap(mm);
+ mm_local_region_free(mm);
}
#ifdef CONFIG_X86_64
diff --git a/arch/x86/include/asm/pgtable_32_areas.h b/arch/x86/include/asm/pgtable_32_areas.h
index 921148b429676..7fccb887f8b33 100644
--- a/arch/x86/include/asm/pgtable_32_areas.h
+++ b/arch/x86/include/asm/pgtable_32_areas.h
@@ -30,9 +30,14 @@ extern bool __vmalloc_start_set; /* set once high_memory is set */
#define CPU_ENTRY_AREA_BASE \
((FIXADDR_TOT_START - PAGE_SIZE*(CPU_ENTRY_AREA_PAGES+1)) & PMD_MASK)
-#define LDT_BASE_ADDR \
- ((CPU_ENTRY_AREA_BASE - PAGE_SIZE) & PMD_MASK)
+/*
+ * On 32-bit the mm-local region is currently completely consumed by the LDT
+ * remap.
+ */
+#define MM_LOCAL_BASE_ADDR ((CPU_ENTRY_AREA_BASE - PAGE_SIZE) & PMD_MASK)
+#define MM_LOCAL_END_ADDR (MM_LOCAL_BASE_ADDR + PMD_SIZE)
+#define LDT_BASE_ADDR MM_LOCAL_BASE_ADDR
#define LDT_END_ADDR (LDT_BASE_ADDR + PMD_SIZE)
#define PKMAP_BASE \
diff --git a/arch/x86/include/asm/pgtable_64_types.h b/arch/x86/include/asm/pgtable_64_types.h
index 7eb61ef6a185f..1181565966405 100644
--- a/arch/x86/include/asm/pgtable_64_types.h
+++ b/arch/x86/include/asm/pgtable_64_types.h
@@ -5,8 +5,11 @@
#include <asm/sparsemem.h>
#ifndef __ASSEMBLER__
+#include <linux/build_bug.h>
#include <linux/types.h>
#include <asm/kaslr.h>
+#include <asm/page_types.h>
+#include <uapi/asm/ldt.h>
/*
* These are used to make use of C type-checking..
@@ -100,9 +103,12 @@ extern unsigned int ptrs_per_p4d;
#define GUARD_HOLE_BASE_ADDR (GUARD_HOLE_PGD_ENTRY << PGDIR_SHIFT)
#define GUARD_HOLE_END_ADDR (GUARD_HOLE_BASE_ADDR + GUARD_HOLE_SIZE)
-#define LDT_PGD_ENTRY -240UL
-#define LDT_BASE_ADDR (LDT_PGD_ENTRY << PGDIR_SHIFT)
-#define LDT_END_ADDR (LDT_BASE_ADDR + PGDIR_SIZE)
+#define MM_LOCAL_PGD_ENTRY -240UL
+#define MM_LOCAL_BASE_ADDR (MM_LOCAL_PGD_ENTRY << PGDIR_SHIFT)
+#define MM_LOCAL_END_ADDR ((MM_LOCAL_PGD_ENTRY + 1) << PGDIR_SHIFT)
+
+#define LDT_BASE_ADDR MM_LOCAL_BASE_ADDR
+#define LDT_END_ADDR (LDT_BASE_ADDR + PMD_SIZE)
#define __VMALLOC_BASE_L4 0xffffc90000000000UL
#define __VMALLOC_BASE_L5 0xffa0000000000000UL
diff --git a/arch/x86/mm/pgtable.c b/arch/x86/mm/pgtable.c
index 8f3505b1eefec..cc768ea8c19c8 100644
--- a/arch/x86/mm/pgtable.c
+++ b/arch/x86/mm/pgtable.c
@@ -335,6 +335,9 @@ pgd_t *pgd_alloc(struct mm_struct *mm)
void pgd_free(struct mm_struct *mm, pgd_t *pgd)
{
+ /* Should be cleaned up in mmap exit path. */
+ VM_WARN_ON_ONCE(mm_local_region_used(mm));
+
pgd_mop_up_pmds(mm, pgd);
pgd_dtor(pgd);
paravirt_pgd_free(mm, pgd);
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 7fabe6c66b4b7..28022c96edcb5 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -989,6 +989,19 @@ static inline void mm_flags_clear_all(struct mm_struct *mm)
bitmap_zero(ACCESS_PRIVATE(&mm->flags, __mm_flags), NUM_MM_FLAG_BITS);
}
+#ifdef CONFIG_MM_LOCAL_REGION
+static inline bool mm_local_region_used(struct mm_struct *mm)
+{
+ return mm_flags_test(MMF_LOCAL_REGION_USED, mm);
+}
+#else
+static inline bool mm_local_region_used(struct mm_struct *mm)
+{
+ VM_WARN_ON_ONCE(mm_flags_test(MMF_LOCAL_REGION_USED, mm));
+ return false;
+}
+#endif
+
extern const struct vm_operations_struct vma_dummy_vm_ops;
static inline void vma_init(struct vm_area_struct *vma, struct mm_struct *mm)
diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index b5d4cd3b067bf..d39fddf57edc8 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -1997,6 +1997,8 @@ enum {
#define MMF_TOPDOWN 31 /* mm searches top down by default */
#define MMF_TOPDOWN_MASK BIT(MMF_TOPDOWN)
+#define MMF_LOCAL_REGION_USED 32
+
#define MMF_INIT_LEGACY_MASK (MMF_DUMP_FILTER_MASK |\
MMF_DISABLE_THP_MASK | MMF_HAS_MDWE_MASK |\
MMF_VM_MERGE_ANY_MASK | MMF_TOPDOWN_MASK)
diff --git a/kernel/fork.c b/kernel/fork.c
index f0e2e131a9a5a..4fb23ea33b7da 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -1154,6 +1154,7 @@ static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
fail_nocontext:
mm_free_id(mm);
fail_noid:
+ WARN_ON_ONCE(mm_local_region_used(mm));
mm_free_pgd(mm);
fail_mm_init:
free_mm(mm);
diff --git a/mm/Kconfig b/mm/Kconfig
index c52ab6afcb159..cb531c1436f77 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -1500,6 +1500,13 @@ config LAZY_MMU_MODE_KUNIT_TEST
If unsure, say N.
+config ARCH_SUPPORTS_MM_LOCAL_REGION
+ bool
+
+config MM_LOCAL_REGION
+ bool
+ depends on ARCH_SUPPORTS_MM_LOCAL_REGION
+
source "mm/damon/Kconfig"
endmenu
--
2.54.0