The splitting and merging of kernel page table mappings between small and large is protected by cpa_lock. The merging is relatively new but the splitting is ancient. The splitting has a locking optimization: since DEBUG_PAGEALLOC forces all mappings to 4k, there are no large pages to split. So the code that *might* cause a split can just skip the locking (and a few other things). This is entertaining, but it adds complexity and makes for weird locking rules. Plus it's all for a debugging feature which makes the kernel super slow in the first place. Optimizing something which is already super slow and not used in production is not the best way to spend our complexity budget. Stop gating cpa_lock on debug_pagealloc_enabled() to simplify the code and the locking rules. [ dhansen: flesh out changelog ] Suggested-by: Dave Hansen Signed-off-by: Mike Rapoport (Microsoft) Signed-off-by: Dave Hansen Link: https://patch.msgid.link/20260715144519.934289-1-rppt@kernel.org Link: https://lore.kernel.org/all/aab44f08-89f8-47fe-bee4-0ab6b25968c6@intel.com/ --- arch/x86/mm/pat/set_memory.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index d023a40a1e03..e8316f5ffa8a 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -62,10 +62,9 @@ enum cpa_warn { static const int cpa_warn_level = CPA_PROTECT; /* - * Serialize cpa() (for !DEBUG_PAGEALLOC which uses large identity mappings) - * using cpa_lock. So that we don't allow any other cpu, with stale large tlb - * entries change the page attribute in parallel to some other cpu - * splitting a large page entry along with changing the attribute. + * Serialize cpa() using cpa_lock so that we don't allow any other cpu, with + * stale large tlb entries, to change the page attribute in parallel to some + * other cpu splitting a large page entry along with changing the attribute. */ static DEFINE_SPINLOCK(cpa_lock); @@ -1235,11 +1234,9 @@ static int split_large_page(struct cpa_data *cpa, pte_t *kpte, { struct ptdesc *ptdesc; - if (!debug_pagealloc_enabled()) - spin_unlock(&cpa_lock); + spin_unlock(&cpa_lock); ptdesc = pagetable_alloc(GFP_KERNEL, 0); - if (!debug_pagealloc_enabled()) - spin_lock(&cpa_lock); + spin_lock(&cpa_lock); if (!ptdesc) return -ENOMEM; @@ -2023,11 +2020,9 @@ static int __change_page_attr_set_clr(struct cpa_data *cpa, int primary) if (cpa->flags & (CPA_ARRAY | CPA_PAGES_ARRAY)) cpa->numpages = 1; - if (!debug_pagealloc_enabled()) - spin_lock(&cpa_lock); + spin_lock(&cpa_lock); ret = __change_page_attr(cpa, primary); - if (!debug_pagealloc_enabled()) - spin_unlock(&cpa_lock); + spin_unlock(&cpa_lock); if (ret) goto out; -- 2.53.0 lookup_address_in_pgd_attr() accumulates the effective NX and RW bits of the walked page table levels so that verify_rwx() can detect mappings that are both writable and executable. The RW bits are folded into a bool with rw &= pXd_flags(*pXd) & _PAGE_RW; but _PAGE_RW is 0x2. So consider the accumulation line: rw &= pXd_flags(*pXd) & _PAGE_RW; where rw=0x1 and the right side evaluates down to 0x2. It'll end up doing: rw = 0x1 & 0x2 and rw always ends up 0. This way rw becomes false at the first level walked, regardless of the actual permissions, and verify_rwx() treats every mapping as non-writable and never reports a W^X violation. Add double negation to the right side to normalize the _PAGE_RW flag to 0 or 1. Fixes: ceb647b4b529 ("x86/pat: Introduce lookup_address_in_pgd_attr()") Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) Reviewed-by: Juergen Gross --- arch/x86/mm/pat/set_memory.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index e8316f5ffa8a..9382f1d6194f 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -730,7 +730,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, *level = PG_LEVEL_512G; *nx |= pgd_flags(*pgd) & _PAGE_NX; - *rw &= pgd_flags(*pgd) & _PAGE_RW; + *rw &= !!(pgd_flags(*pgd) & _PAGE_RW); p4d = p4d_offset(pgd, address); if (p4d_none(*p4d)) @@ -741,7 +741,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, *level = PG_LEVEL_1G; *nx |= p4d_flags(*p4d) & _PAGE_NX; - *rw &= p4d_flags(*p4d) & _PAGE_RW; + *rw &= !!(p4d_flags(*p4d) & _PAGE_RW); pud = pud_offset(p4d, address); if (pud_none(*pud)) @@ -752,7 +752,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, *level = PG_LEVEL_2M; *nx |= pud_flags(*pud) & _PAGE_NX; - *rw &= pud_flags(*pud) & _PAGE_RW; + *rw &= !!(pud_flags(*pud) & _PAGE_RW); pmd = pmd_offset(pud, address); if (pmd_none(*pmd)) @@ -763,7 +763,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, *level = PG_LEVEL_4K; *nx |= pmd_flags(*pmd) & _PAGE_NX; - *rw &= pmd_flags(*pmd) & _PAGE_RW; + *rw &= !!(pmd_flags(*pmd) & _PAGE_RW); return pte_offset_kernel(pmd, address); } -- 2.53.0 change_page_attr() implementation uses x86-specific enum pg_level to track page table levels. In preparation to moving core parts of change_page_attr() to common code, replace usage of enum pg_level and it's values with the generic enum pgtable_level and the values it defines. Extend the generic enum pgtable_level with PGTABLE_LEVEL_NONE and PGTABLE_LEVEL_NUM to match the numeric values from enum pg_level. This is safe because nothing relies on the numeric values of PGTABLE_LEVEL_* constants. No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 56 ++++++++++++++++++++++---------------------- include/linux/pgtable.h | 6 ++++- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 9382f1d6194f..328805933d4d 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -80,7 +80,7 @@ static inline pgprot_t cachemode2pgprot(enum page_cache_mode pcm) } #ifdef CONFIG_PROC_FS -static unsigned long direct_pages_count[PG_LEVEL_NUM]; +static unsigned long direct_pages_count[PGTABLE_LEVEL_NUM]; void update_page_count(int level, unsigned long pages) { @@ -97,9 +97,9 @@ static void split_page_count(int level) direct_pages_count[level]--; if (system_state == SYSTEM_RUNNING) { - if (level == PG_LEVEL_2M) + if (level == PGTABLE_LEVEL_PMD) count_vm_event(DIRECT_MAP_LEVEL2_SPLIT); - else if (level == PG_LEVEL_1G) + else if (level == PGTABLE_LEVEL_PUD) count_vm_event(DIRECT_MAP_LEVEL3_SPLIT); } direct_pages_count[level - 1] += PTRS_PER_PTE; @@ -109,9 +109,9 @@ static void collapse_page_count(int level) { direct_pages_count[level]++; if (system_state == SYSTEM_RUNNING) { - if (level == PG_LEVEL_2M) + if (level == PGTABLE_LEVEL_PMD) count_vm_event(DIRECT_MAP_LEVEL2_COLLAPSE); - else if (level == PG_LEVEL_1G) + else if (level == PGTABLE_LEVEL_PUD) count_vm_event(DIRECT_MAP_LEVEL3_COLLAPSE); } direct_pages_count[level - 1] -= PTRS_PER_PTE; @@ -120,17 +120,17 @@ static void collapse_page_count(int level) void arch_report_meminfo(struct seq_file *m) { seq_printf(m, "DirectMap4k: %8lu kB\n", - direct_pages_count[PG_LEVEL_4K] << 2); + direct_pages_count[PGTABLE_LEVEL_PTE] << 2); #if defined(CONFIG_X86_64) || defined(CONFIG_X86_PAE) seq_printf(m, "DirectMap2M: %8lu kB\n", - direct_pages_count[PG_LEVEL_2M] << 11); + direct_pages_count[PGTABLE_LEVEL_PMD] << 11); #else seq_printf(m, "DirectMap4M: %8lu kB\n", - direct_pages_count[PG_LEVEL_2M] << 12); + direct_pages_count[PGTABLE_LEVEL_PMD] << 12); #endif if (direct_gbpages) seq_printf(m, "DirectMap1G: %8lu kB\n", - direct_pages_count[PG_LEVEL_1G] << 20); + direct_pages_count[PGTABLE_LEVEL_PUD] << 20); } #else static inline void split_page_count(int level) { } @@ -164,7 +164,7 @@ static inline void cpa_inc_4k_install(void) static inline void cpa_inc_lp_sameprot(int level) { - if (level == PG_LEVEL_1G) + if (level == PGTABLE_LEVEL_PUD) cpa_1g_sameprot++; else cpa_2m_sameprot++; @@ -172,7 +172,7 @@ static inline void cpa_inc_lp_sameprot(int level) static inline void cpa_inc_lp_preserved(int level) { - if (level == PG_LEVEL_1G) + if (level == PGTABLE_LEVEL_PUD) cpa_1g_preserved++; else cpa_2m_preserved++; @@ -577,7 +577,7 @@ static pgprotval_t protect_kernel_text_ro(unsigned long start, * so the protections for kernel text and identity mappings have to * be the same. */ - if (lookup_address(start, &level) && (level != PG_LEVEL_4K)) + if (lookup_address(start, &level) && (level != PGTABLE_LEVEL_PTE)) return _PAGE_RW; return 0; } @@ -721,14 +721,14 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, pud_t *pud; pmd_t *pmd; - *level = PG_LEVEL_256T; + *level = PGTABLE_LEVEL_PGD; *nx = false; *rw = true; if (pgd_none(*pgd)) return NULL; - *level = PG_LEVEL_512G; + *level = PGTABLE_LEVEL_P4D; *nx |= pgd_flags(*pgd) & _PAGE_NX; *rw &= !!(pgd_flags(*pgd) & _PAGE_RW); @@ -739,7 +739,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, if (p4d_leaf(*p4d) || !p4d_present(*p4d)) return (pte_t *)p4d; - *level = PG_LEVEL_1G; + *level = PGTABLE_LEVEL_PUD; *nx |= p4d_flags(*p4d) & _PAGE_NX; *rw &= !!(p4d_flags(*p4d) & _PAGE_RW); @@ -750,7 +750,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, if (pud_leaf(*pud) || !pud_present(*pud)) return (pte_t *)pud; - *level = PG_LEVEL_2M; + *level = PGTABLE_LEVEL_PMD; *nx |= pud_flags(*pud) & _PAGE_NX; *rw &= !!(pud_flags(*pud) & _PAGE_RW); @@ -761,7 +761,7 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, if (pmd_leaf(*pmd) || !pmd_present(*pmd)) return (pte_t *)pmd; - *level = PG_LEVEL_4K; + *level = PGTABLE_LEVEL_PTE; *nx |= pmd_flags(*pmd) & _PAGE_NX; *rw &= !!(pmd_flags(*pmd) & _PAGE_RW); @@ -849,9 +849,9 @@ pmd_t *lookup_pmd_address(unsigned long address) phys_addr_t slow_virt_to_phys(void *__virt_addr) { unsigned long virt_addr = (unsigned long)__virt_addr; + enum pgtable_level level; phys_addr_t phys_addr; unsigned long offset; - enum pg_level level; pte_t *pte; pte = lookup_address(virt_addr, &level); @@ -863,11 +863,11 @@ phys_addr_t slow_virt_to_phys(void *__virt_addr) * make 32-PAE kernel work correctly. */ switch (level) { - case PG_LEVEL_1G: + case PGTABLE_LEVEL_PUD: phys_addr = (phys_addr_t)pud_pfn(*(pud_t *)pte) << PAGE_SHIFT; offset = virt_addr & ~PUD_MASK; break; - case PG_LEVEL_2M: + case PGTABLE_LEVEL_PMD: phys_addr = (phys_addr_t)pmd_pfn(*(pmd_t *)pte) << PAGE_SHIFT; offset = virt_addr & ~PMD_MASK; break; @@ -929,8 +929,8 @@ static int __should_split_large_page(pte_t *kpte, unsigned long address, { unsigned long numpages, pmask, psize, lpaddr, pfn, old_pfn; pgprot_t old_prot, new_prot, req_prot, chk_prot; + enum pgtable_level level; pte_t new_pte, *tmp; - enum pg_level level; bool nx, rw; /* @@ -942,12 +942,12 @@ static int __should_split_large_page(pte_t *kpte, unsigned long address, return 1; switch (level) { - case PG_LEVEL_2M: + case PGTABLE_LEVEL_PMD: old_prot = pmd_pgprot(*(pmd_t *)kpte); old_pfn = pmd_pfn(*(pmd_t *)kpte); cpa_inc_2m_checked(); break; - case PG_LEVEL_1G: + case PGTABLE_LEVEL_PUD: old_prot = pud_pgprot(*(pud_t *)kpte); old_pfn = pud_pfn(*(pud_t *)kpte); cpa_inc_1g_checked(); @@ -1148,7 +1148,7 @@ __split_large_page(struct cpa_data *cpa, pte_t *kpte, unsigned long address, paravirt_alloc_pte(&init_mm, page_to_pfn(base)); switch (level) { - case PG_LEVEL_2M: + case PGTABLE_LEVEL_PMD: ref_prot = pmd_pgprot(*(pmd_t *)kpte); /* * Clear PSE (aka _PAGE_PAT) and move @@ -1160,7 +1160,7 @@ __split_large_page(struct cpa_data *cpa, pte_t *kpte, unsigned long address, lpinc = PAGE_SIZE; break; - case PG_LEVEL_1G: + case PGTABLE_LEVEL_PUD: ref_prot = pud_pgprot(*(pud_t *)kpte); ref_pfn = pud_pfn(*(pud_t *)kpte); pfninc = PMD_SIZE >> PAGE_SHIFT; @@ -1311,7 +1311,7 @@ static int collapse_pmd_page(pmd_t *pmd, unsigned long addr, } if (virt_addr_valid(addr) && pfn_range_is_mapped(pfn, pfn + 1)) - collapse_page_count(PG_LEVEL_2M); + collapse_page_count(PGTABLE_LEVEL_PMD); return 1; } @@ -1358,7 +1358,7 @@ static int collapse_pud_page(pud_t *pud, unsigned long addr, set_pud(pud, pfn_pud(pfn, pmd_pgprot(first))); if (virt_addr_valid(addr) && pfn_range_is_mapped(pfn, pfn + 1)) - collapse_page_count(PG_LEVEL_1G); + collapse_page_count(PGTABLE_LEVEL_PUD); return 1; } @@ -1860,7 +1860,7 @@ static int __change_page_attr(struct cpa_data *cpa, int primary) if (pte_none(old_pte)) return __cpa_process_fault(cpa, address, primary); - if (level == PG_LEVEL_4K) { + if (level == PGTABLE_LEVEL_PTE) { pte_t new_pte; pgprot_t old_prot = pte_pgprot(old_pte); pgprot_t new_prot = pte_pgprot(old_pte); diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h index 2981e386da7b..3889bc2a1f76 100644 --- a/include/linux/pgtable.h +++ b/include/linux/pgtable.h @@ -2295,16 +2295,20 @@ static inline bool arch_has_pfn_modify_check(void) typedef unsigned int pgtbl_mod_mask; enum pgtable_level { - PGTABLE_LEVEL_PTE = 0, + PGTABLE_LEVEL_NONE = 0, + PGTABLE_LEVEL_PTE, PGTABLE_LEVEL_PMD, PGTABLE_LEVEL_PUD, PGTABLE_LEVEL_P4D, PGTABLE_LEVEL_PGD, + PGTABLE_LEVEL_NUM, }; static inline const char *pgtable_level_to_str(enum pgtable_level level) { switch (level) { + case PGTABLE_LEVEL_NONE: + return "none"; case PGTABLE_LEVEL_PTE: return "pte"; case PGTABLE_LEVEL_PMD: -- 2.53.0 In preparation to moving core parts of change_page_attr() to common code, replace open coded checks for _PAGE_RW and _PAGE_NX bits in lookup_address_in_pgd_attr() with pXd_write() and pXd_exec() accessors. Add x86 implementation of pXd_write() and pXd_exec() accessors and provide generic stubs in include/linux/pgtable.h. The stubs follow the existing pattern and BUG() if they are called. No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/include/asm/pgtable.h | 36 ++++++++++++++++++++++++++++++++++++ arch/x86/mm/pat/set_memory.c | 16 ++++++++-------- include/linux/pgtable.h | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/arch/x86/include/asm/pgtable.h b/arch/x86/include/asm/pgtable.h index ac295ca6c92f..497aaf08ad36 100644 --- a/arch/x86/include/asm/pgtable.h +++ b/arch/x86/include/asm/pgtable.h @@ -227,6 +227,42 @@ static inline int pud_write(pud_t pud) return pud_flags(pud) & _PAGE_RW; } +#define p4d_write p4d_write +static inline int p4d_write(p4d_t p4d) +{ + return p4d_flags(p4d) & _PAGE_RW; +} + +#define pgd_write pgd_write +static inline int pgd_write(pgd_t pgd) +{ + return pgd_flags(pgd) & _PAGE_RW; +} + +#define pmd_exec pmd_exec +static inline int pmd_exec(pmd_t pmd) +{ + return !(pmd_flags(pmd) & _PAGE_NX); +} + +#define pud_exec pud_exec +static inline int pud_exec(pud_t pud) +{ + return !(pud_flags(pud) & _PAGE_NX); +} + +#define p4d_exec p4d_exec +static inline int p4d_exec(p4d_t p4d) +{ + return !(p4d_flags(p4d) & _PAGE_NX); +} + +#define pgd_exec pgd_exec +static inline int pgd_exec(pgd_t pgd) +{ + return !(pgd_flags(pgd) & _PAGE_NX); +} + static inline int pte_huge(pte_t pte) { return pte_flags(pte) & _PAGE_PSE; diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 328805933d4d..8a1782f5de3e 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -729,8 +729,8 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, return NULL; *level = PGTABLE_LEVEL_P4D; - *nx |= pgd_flags(*pgd) & _PAGE_NX; - *rw &= !!(pgd_flags(*pgd) & _PAGE_RW); + *nx |= !pgd_exec(*pgd); + *rw &= !!(pgd_write(*pgd)); p4d = p4d_offset(pgd, address); if (p4d_none(*p4d)) @@ -740,8 +740,8 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, return (pte_t *)p4d; *level = PGTABLE_LEVEL_PUD; - *nx |= p4d_flags(*p4d) & _PAGE_NX; - *rw &= !!(p4d_flags(*p4d) & _PAGE_RW); + *nx |= !p4d_exec(*p4d); + *rw &= !!(p4d_write(*p4d)); pud = pud_offset(p4d, address); if (pud_none(*pud)) @@ -751,8 +751,8 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, return (pte_t *)pud; *level = PGTABLE_LEVEL_PMD; - *nx |= pud_flags(*pud) & _PAGE_NX; - *rw &= !!(pud_flags(*pud) & _PAGE_RW); + *nx |= !pud_exec(*pud); + *rw &= !!(pud_write(*pud)); pmd = pmd_offset(pud, address); if (pmd_none(*pmd)) @@ -762,8 +762,8 @@ pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, return (pte_t *)pmd; *level = PGTABLE_LEVEL_PTE; - *nx |= pmd_flags(*pmd) & _PAGE_NX; - *rw &= !!(pmd_flags(*pmd) & _PAGE_RW); + *nx |= !pmd_exec(*pmd); + *rw &= !!(pmd_write(*pmd)); return pte_offset_kernel(pmd, address); } diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h index 3889bc2a1f76..4b42ac5c27d9 100644 --- a/include/linux/pgtable.h +++ b/include/linux/pgtable.h @@ -2105,6 +2105,46 @@ static inline int pud_write(pud_t pud) } #endif /* pud_write */ +#ifndef p4d_write +static inline int p4d_write(p4d_t p4d) +{ + BUG(); + return 0; +} +#endif /* p4d_write */ + +#ifndef pmd_exec +static inline int pmd_exec(pmd_t pmd) +{ + BUG(); + return 0; +} +#endif /* pmd_exec */ + +#ifndef pud_exec +static inline int pud_exec(pud_t pud) +{ + BUG(); + return 0; +} +#endif /* pud_exec */ + +#ifndef p4d_exec +static inline int p4d_exec(p4d_t p4d) +{ + BUG(); + return 0; +} +#endif /* p4d_exec */ + +#ifndef pgd_exec +static inline int pgd_exec(pgd_t pgd) +{ + BUG(); + return 0; +} +#endif /* pgd_exec */ + #if !defined(CONFIG_TRANSPARENT_HUGEPAGE) || \ !defined(CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD) static inline int pud_trans_huge(pud_t pud) -- 2.53.0 __should_split_large_page() uses x86 specific page_level_size() to calculate the size and mask of the large page. In preparation to moving core parts of change_page_attr() to common code, introduce generic pgtable_level_size() and pgtable_level_mask() helpers and use them in __should_split_large_page(). No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 4 ++-- include/linux/pgtable.h | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 8a1782f5de3e..ee40962d4ee9 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -956,8 +956,8 @@ static int __should_split_large_page(pte_t *kpte, unsigned long address, return -EINVAL; } - psize = page_level_size(level); - pmask = page_level_mask(level); + psize = pgtable_level_size(level); + pmask = pgtable_level_mask(level); /* * Calculate the number of pages, which fit into this large diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h index 4b42ac5c27d9..9eb00ad6d5d2 100644 --- a/include/linux/pgtable.h +++ b/include/linux/pgtable.h @@ -2364,6 +2364,28 @@ static inline const char *pgtable_level_to_str(enum pgtable_level level) } } +/* Size of the memory mapped by a single leaf entry at @level */ +static inline unsigned long pgtable_level_size(enum pgtable_level level) +{ + switch (level) { + case PGTABLE_LEVEL_PTE: + return PAGE_SIZE; + case PGTABLE_LEVEL_PMD: + return PMD_SIZE; + case PGTABLE_LEVEL_PUD: + return PUD_SIZE; + case PGTABLE_LEVEL_P4D: + return P4D_SIZE; + default: + return PGDIR_SIZE; + } +} + +static inline unsigned long pgtable_level_mask(enum pgtable_level level) +{ + return ~(pgtable_level_size(level) - 1); +} + #endif /* !__ASSEMBLY__ */ #if !defined(MAX_POSSIBLE_PHYSMEM_BITS) && !defined(CONFIG_64BIT) -- 2.53.0 Once core parts of change_page_attr() would be moved to common code, the processing of alias mappings would be out of line and would require a function call for each modified PTE even if the primary target of change_page_attr() does not have alias mappings. Calling a function per PTE just to find out it has nothing to do is expensive and resulted in ~30% regression in instrumented cpa-test. Although cpa-test is a microbenchmark, ~30% regression is still a lot and can be easily avoided by inlining the checks that gates processing of the alias mapping. Introduce cpa_should_update_alias() inline helper that checks if the alias update is required. Since on 64-bit an alias could be in the high kernel mapping, pull __cpa_pfn_in_highmap() along and rename it to pfn_is_kernel(). No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/include/asm/set_memory.h | 50 +++++++++++++++++++++++++++++++ arch/x86/mm/pat/set_memory.c | 62 +++------------------------------------ 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/arch/x86/include/asm/set_memory.h b/arch/x86/include/asm/set_memory.h index 4362c26aa992..332ec300c2ed 100644 --- a/arch/x86/include/asm/set_memory.h +++ b/arch/x86/include/asm/set_memory.h @@ -3,12 +3,62 @@ #define _ASM_X86_SET_MEMORY_H #include +#include #include #include #define set_memory_rox set_memory_rox int set_memory_rox(unsigned long addr, int numpages); +#ifdef CONFIG_X86_64 +/* + * The kernel image is mapped into two places in the virtual address space + * (addresses without KASLR, of course): + * + * 1. The kernel direct map (0xffff880000000000) + * 2. The "high kernel map" (0xffffffff81000000) + * + * We actually execute out of #2. If we get the address of a kernel symbol, it + * points to #2, but almost all physical-to-virtual translations point to #1. + * + * This is so that we can have both a directmap of all physical memory *and* + * take full advantage of the limited (s32) immediate addressing range (2G) + * of x86_64. + * + * See Documentation/arch/x86/x86_64/mm.rst for more detail. + */ +static inline bool pfn_is_kernel(unsigned long pfn) +{ + unsigned long spfn = __pa_symbol(_text) >> PAGE_SHIFT; + /* Do not reference a physical address outside the kernel. */ + unsigned long epfn = __pa_symbol(roundup(_brk_end, PMD_SIZE) - 1) >> PAGE_SHIFT; + + return pfn >= spfn && pfn <= epfn; +} +#else +static inline bool pfn_is_kernel(unsigned long pfn) +{ + /* There is no highmap on 32-bit */ + return false; +} +#endif + +static inline bool cpa_should_update_alias(unsigned long vaddr, + unsigned long pfn) +{ + /* Primary is not in the direct map, its direct map alias needs update */ + if (vaddr < PAGE_OFFSET || + vaddr >= PAGE_OFFSET + (max_pfn_mapped << PAGE_SHIFT)) + return true; + + /* direct map page that is also part of the kernel highmap */ + if ((vaddr < (unsigned long)_text || vaddr >= _brk_end) && + pfn_is_kernel(pfn)) + return true; + + return false; +} + /* * The set_memory_* API can be used to change various attributes of a virtual * address range. The attributes include: diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index ee40962d4ee9..2a087dcedf84 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -224,61 +224,6 @@ within(unsigned long addr, unsigned long start, unsigned long end) return addr >= start && addr < end; } -#ifdef CONFIG_X86_64 - -static inline int -within_inclusive(unsigned long addr, unsigned long start, unsigned long end) -{ - return addr >= start && addr <= end; -} - -/* - * The kernel image is mapped into two places in the virtual address space - * (addresses without KASLR, of course): - * - * 1. The kernel direct map (0xffff880000000000) - * 2. The "high kernel map" (0xffffffff81000000) - * - * We actually execute out of #2. If we get the address of a kernel symbol, it - * points to #2, but almost all physical-to-virtual translations point to #1. - * - * This is so that we can have both a directmap of all physical memory *and* - * take full advantage of the limited (s32) immediate addressing range (2G) - * of x86_64. - * - * See Documentation/arch/x86/x86_64/mm.rst for more detail. - */ - -static inline unsigned long highmap_start_pfn(void) -{ - return __pa_symbol(_text) >> PAGE_SHIFT; -} - -static inline unsigned long highmap_end_pfn(void) -{ - /* Do not reference physical address outside the kernel. */ - return __pa_symbol(roundup(_brk_end, PMD_SIZE) - 1) >> PAGE_SHIFT; -} - -static bool __cpa_pfn_in_highmap(unsigned long pfn) -{ - /* - * Kernel text has an alias mapping at a high address, known - * here as "highmap". - */ - return within_inclusive(pfn, highmap_start_pfn(), highmap_end_pfn()); -} - -#else - -static bool __cpa_pfn_in_highmap(unsigned long pfn) -{ - /* There is no highmap on 32-bit */ - return false; -} - -#endif - /* * See set_mce_nospec(). * @@ -1830,7 +1775,7 @@ static int __cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, cpa->pfn = __pa(vaddr) >> PAGE_SHIFT; return 0; - } else if (__cpa_pfn_in_highmap(cpa->pfn)) { + } else if (pfn_is_kernel(cpa->pfn)) { /* Faults in the highmap are OK, so do not warn: */ return -EFAULT; } else { @@ -1968,7 +1913,7 @@ static int cpa_process_alias(struct cpa_data *cpa) * to touch the high mapped kernel as well: */ if (!within(vaddr, (unsigned long)_text, _brk_end) && - __cpa_pfn_in_highmap(cpa->pfn)) { + pfn_is_kernel(cpa->pfn)) { unsigned long temp_cpa_vaddr = (cpa->pfn << PAGE_SHIFT) + __START_KERNEL_map - phys_base; alias_cpa = *cpa; @@ -2026,7 +1971,8 @@ static int __change_page_attr_set_clr(struct cpa_data *cpa, int primary) if (ret) goto out; - if (primary && !(cpa->flags & CPA_NO_CHECK_ALIAS)) { + if (primary && !(cpa->flags & CPA_NO_CHECK_ALIAS) && + cpa_should_update_alias(__cpa_addr(cpa, cpa->curpage), cpa->pfn)) { ret = cpa_process_alias(cpa); if (ret) goto out; -- 2.53.0 Currently should_split_large_page() and split_large_page() duplicate some of the operations, like PTE lookup inside the lock including detection of NX and RW bits and large page size calculation. This duplication can be avoided if the split parameters are passed between these two functions in a structure and the page table allocation and locking order is slightly reorganized. As the first step in this refactoring, introduce cpa_handle_large_page() helper that wraps should_split_large_page() and split_large_page(). No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 54 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 2a087dcedf84..a9d270e986ff 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -1191,6 +1191,35 @@ static int split_large_page(struct cpa_data *cpa, pte_t *kpte, return 0; } +static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, + unsigned long address) +{ + int do_split, err; + + /* + * Check, whether we can keep the large page intact + * and just change the pte: + */ + do_split = should_split_large_page(kpte, address, cpa); + /* + * When the range fits into the existing large page, no split is + * required. + * should_split_large_page() updated the large page attributes and + * cpa->numpages and cpa->pfn. + */ + if (do_split <= 0) + return do_split; + + /* + * We have to split the large page: + */ + err = split_large_page(cpa, kpte, address); + if (err) + return err; + + return do_split; +} + static int collapse_pmd_page(pmd_t *pmd, unsigned long addr, struct list_head *pgtables) { @@ -1790,7 +1819,7 @@ static int __cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, static int __change_page_attr(struct cpa_data *cpa, int primary) { unsigned long address; - int do_split, err; + int split_res; unsigned int level; pte_t *kpte, old_pte; bool nx, rw; @@ -1842,27 +1871,12 @@ static int __change_page_attr(struct cpa_data *cpa, int primary) return 0; } - /* - * Check, whether we can keep the large page intact - * and just change the pte: - */ - do_split = should_split_large_page(kpte, address, cpa); - /* - * When the range fits into the existing large page, - * return. cp->numpages and cpa->tlbflush have been updated in - * try_large_page: - */ - if (do_split <= 0) - return do_split; - - /* - * We have to split the large page: - */ - err = split_large_page(cpa, kpte, address); - if (!err) + /* Update large page or split it */ + split_res = cpa_handle_large_page(cpa, kpte, address); + if (split_res > 0) goto repeat; - return err; + return split_res; } static int __change_page_attr_set_clr(struct cpa_data *cpa, int primary); -- 2.53.0 Keeping all split parameters in a struct between should_split_large_page() and split_large_page() allows maintaining the split state that is initially established in cpa_handle_large_page(), then this state is updated in should_split_large_page() and finally consumed by split_large_page(). Start passing parameters between should_split_large_page() and split_large_page() in cpa_split_data structure. As the first step only pass the existing parameters in the new structure. No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 49 ++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index a9d270e986ff..c2a72cd8b832 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -53,6 +53,15 @@ struct cpa_data { struct page **pages; }; +/* + * context for checking for a split a large page and splitting it if required. + */ +struct cpa_split_data { + unsigned long address; + pte_t *kpte; + struct ptdesc *ptdesc; +}; + enum cpa_warn { CPA_CONFLICT, CPA_PROTECT, @@ -869,12 +878,14 @@ static pgprot_t pgprot_clear_protnone_bits(pgprot_t prot) return prot; } -static int __should_split_large_page(pte_t *kpte, unsigned long address, - struct cpa_data *cpa) +static int __should_split_large_page(struct cpa_data *cpa, + struct cpa_split_data *sd) { unsigned long numpages, pmask, psize, lpaddr, pfn, old_pfn; pgprot_t old_prot, new_prot, req_prot, chk_prot; + unsigned long address = sd->address; enum pgtable_level level; + pte_t *kpte = sd->kpte; pte_t new_pte, *tmp; bool nx, rw; @@ -1016,8 +1027,8 @@ static int __should_split_large_page(pte_t *kpte, unsigned long address, return 0; } -static int should_split_large_page(pte_t *kpte, unsigned long address, - struct cpa_data *cpa) +static int should_split_large_page(struct cpa_data *cpa, + struct cpa_split_data *sd) { int do_split; @@ -1025,7 +1036,7 @@ static int should_split_large_page(pte_t *kpte, unsigned long address, return 1; spin_lock(&pgd_lock); - do_split = __should_split_large_page(kpte, address, cpa); + do_split = __should_split_large_page(cpa, sd); spin_unlock(&pgd_lock); return do_split; @@ -1067,13 +1078,14 @@ static void split_set_pte(struct cpa_data *cpa, pte_t *pte, unsigned long pfn, set_pte(pte, pfn_pte(pfn, ref_prot)); } -static int -__split_large_page(struct cpa_data *cpa, pte_t *kpte, unsigned long address, - struct ptdesc *ptdesc) +static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { unsigned long lpaddr, lpinc, ref_pfn, pfn, pfninc = 1; + struct ptdesc *ptdesc = sd->ptdesc; struct page *base = ptdesc_page(ptdesc); pte_t *pbase = (pte_t *)page_address(base); + unsigned long address = sd->address; + pte_t *kpte = sd->kpte; unsigned int i, level; pgprot_t ref_prot; bool nx, rw; @@ -1174,19 +1186,16 @@ __split_large_page(struct cpa_data *cpa, pte_t *kpte, unsigned long address, return 0; } -static int split_large_page(struct cpa_data *cpa, pte_t *kpte, - unsigned long address) +static int split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - struct ptdesc *ptdesc; - spin_unlock(&cpa_lock); - ptdesc = pagetable_alloc(GFP_KERNEL, 0); + sd->ptdesc = pagetable_alloc(GFP_KERNEL, 0); spin_lock(&cpa_lock); - if (!ptdesc) + if (!sd->ptdesc) return -ENOMEM; - if (__split_large_page(cpa, kpte, address, ptdesc)) - pagetable_free(ptdesc); + if (__split_large_page(cpa, sd)) + pagetable_free(sd->ptdesc); return 0; } @@ -1194,13 +1203,17 @@ static int split_large_page(struct cpa_data *cpa, pte_t *kpte, static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, unsigned long address) { + struct cpa_split_data sd = { + .address = address, + .kpte = kpte, + }; int do_split, err; /* * Check, whether we can keep the large page intact * and just change the pte: */ - do_split = should_split_large_page(kpte, address, cpa); + do_split = should_split_large_page(cpa, &sd); /* * When the range fits into the existing large page, no split is * required. @@ -1213,7 +1226,7 @@ static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, /* * We have to split the large page: */ - err = split_large_page(cpa, kpte, address); + err = split_large_page(cpa, &sd); if (err) return err; -- 2.53.0 Currently both __should_split_large_page() and __split_large_page() verify that the page table entry wasn't changed by another CPU while pgd_lock was released. pgd_lock must be released between __should_split_large_page() and __split_large_page() because split of a large page requires allocation of a new page table page. Preallocating the page table page before checking if the large page should be split allows running both __should_split_large_page() and __split_large_page() inside the same critical section protected by pgd_lock and allows dropping the duplicated lookup. Move allocation of the page table before the call to should_split_large_page() and pull out race-checking lookup into cpa_handle_large_page(). Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 89 ++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index c2a72cd8b832..ef206ba6e254 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -60,6 +60,9 @@ struct cpa_split_data { unsigned long address; pte_t *kpte; struct ptdesc *ptdesc; + enum pgtable_level level; + bool nx; + bool rw; }; enum cpa_warn { @@ -883,19 +886,12 @@ static int __should_split_large_page(struct cpa_data *cpa, { unsigned long numpages, pmask, psize, lpaddr, pfn, old_pfn; pgprot_t old_prot, new_prot, req_prot, chk_prot; + enum pgtable_level level = sd->level; unsigned long address = sd->address; - enum pgtable_level level; pte_t *kpte = sd->kpte; - pte_t new_pte, *tmp; - bool nx, rw; - - /* - * Check for races, another CPU might have split this page - * up already: - */ - tmp = _lookup_address_cpa(cpa, address, &level, &nx, &rw); - if (tmp != kpte) - return 1; + bool nx = sd->nx; + bool rw = sd->rw; + pte_t new_pte; switch (level) { case PGTABLE_LEVEL_PMD: @@ -1030,16 +1026,10 @@ static int __should_split_large_page(struct cpa_data *cpa, static int should_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - int do_split; - if (cpa->force_split) return 1; - spin_lock(&pgd_lock); - do_split = __should_split_large_page(cpa, sd); - spin_unlock(&pgd_lock); - - return do_split; + return __should_split_large_page(cpa, sd); } static void split_set_pte(struct cpa_data *cpa, pte_t *pte, unsigned long pfn, @@ -1084,23 +1074,11 @@ static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) struct ptdesc *ptdesc = sd->ptdesc; struct page *base = ptdesc_page(ptdesc); pte_t *pbase = (pte_t *)page_address(base); + enum pgtable_level level = sd->level; unsigned long address = sd->address; pte_t *kpte = sd->kpte; - unsigned int i, level; + unsigned int i; pgprot_t ref_prot; - bool nx, rw; - pte_t *tmp; - - spin_lock(&pgd_lock); - /* - * Check for races, another CPU might have split this page - * up for us already: - */ - tmp = _lookup_address_cpa(cpa, address, &level, &nx, &rw); - if (tmp != kpte) { - spin_unlock(&pgd_lock); - return 1; - } paravirt_alloc_pte(&init_mm, page_to_pfn(base)); @@ -1133,7 +1111,6 @@ static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) break; default: - spin_unlock(&pgd_lock); return 1; } @@ -1181,23 +1158,13 @@ static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) * just split large page entry. */ flush_tlb_all(); - spin_unlock(&pgd_lock); return 0; } static int split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - spin_unlock(&cpa_lock); - sd->ptdesc = pagetable_alloc(GFP_KERNEL, 0); - spin_lock(&cpa_lock); - if (!sd->ptdesc) - return -ENOMEM; - - if (__split_large_page(cpa, sd)) - pagetable_free(sd->ptdesc); - - return 0; + return __split_large_page(cpa, sd); } static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, @@ -1207,13 +1174,31 @@ static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, .address = address, .kpte = kpte, }; - int do_split, err; + int do_split, ret = 1; + pte_t *tmp; + + spin_unlock(&cpa_lock); + sd.ptdesc = pagetable_alloc(GFP_KERNEL, 0); + spin_lock(&cpa_lock); + if (!sd.ptdesc) + return -ENOMEM; + + spin_lock(&pgd_lock); + + /* + * Check for races, another CPU might have split this page + * up already: + */ + tmp = _lookup_address_cpa(cpa, address, &sd.level, &sd.nx, &sd.rw); + if (tmp != kpte) + goto out_free_ptdesc; /* * Check, whether we can keep the large page intact * and just change the pte: */ do_split = should_split_large_page(cpa, &sd); + ret = do_split; /* * When the range fits into the existing large page, no split is * required. @@ -1221,16 +1206,22 @@ static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, * cpa->numpages and cpa->pfn. */ if (do_split <= 0) - return do_split; + goto out_free_ptdesc; /* * We have to split the large page: */ - err = split_large_page(cpa, &sd); - if (err) - return err; + ret = split_large_page(cpa, &sd); + if (ret) + goto out_free_ptdesc; + spin_unlock(&pgd_lock); return do_split; + +out_free_ptdesc: + spin_unlock(&pgd_lock); + pagetable_free(sd.ptdesc); + return ret; } static int collapse_pmd_page(pmd_t *pmd, unsigned long addr, -- 2.53.0 The calculation of large page size and mask and the update to cpa->numpages use generic helpers and can be reused by common code. The upcoming changes that will make should_split_large_page() available in common code and __should_split_large_page() will become the basis of x86 specific implementation. In preparation to moving core parts of change_page_attr() to common code, move the generic calculations from __should_split_large_page() to should_split_large_page(). No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/mm/pat/set_memory.c | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index ef206ba6e254..56676d4cf510 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -58,6 +58,8 @@ struct cpa_data { */ struct cpa_split_data { unsigned long address; + unsigned long psize; + unsigned long pmask; pte_t *kpte; struct ptdesc *ptdesc; enum pgtable_level level; @@ -884,10 +886,12 @@ static pgprot_t pgprot_clear_protnone_bits(pgprot_t prot) static int __should_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - unsigned long numpages, pmask, psize, lpaddr, pfn, old_pfn; pgprot_t old_prot, new_prot, req_prot, chk_prot; + unsigned long numpages, lpaddr, pfn, old_pfn; enum pgtable_level level = sd->level; unsigned long address = sd->address; + unsigned long psize = sd->psize; + unsigned long pmask = sd->pmask; pte_t *kpte = sd->kpte; bool nx = sd->nx; bool rw = sd->rw; @@ -908,18 +912,6 @@ static int __should_split_large_page(struct cpa_data *cpa, return -EINVAL; } - psize = pgtable_level_size(level); - pmask = pgtable_level_mask(level); - - /* - * Calculate the number of pages, which fit into this large - * page starting at address: - */ - lpaddr = (address + psize) & pmask; - numpages = (lpaddr - address) >> PAGE_SHIFT; - if (numpages < cpa->numpages) - cpa->numpages = numpages; - /* * We are safe now. Check whether the new pgprot is the same: * Convert protection attributes to 4k-format, as cpa->mask* are set @@ -1026,9 +1018,25 @@ static int __should_split_large_page(struct cpa_data *cpa, static int should_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { + enum pgtable_level level = sd->level; + unsigned long address = sd->address; + unsigned long numpages, lpaddr; + if (cpa->force_split) return 1; + sd->psize = pgtable_level_size(level); + sd->pmask = pgtable_level_mask(level); + + /* + * Calculate the number of pages, which fit into this large + * page starting at address: + */ + lpaddr = (address + sd->psize) & sd->pmask; + numpages = (lpaddr - address) >> PAGE_SHIFT; + if (numpages < cpa->numpages) + cpa->numpages = numpages; + return __should_split_large_page(cpa, sd); } -- 2.53.0 Currently there are 6 independent implementations for set_memory and set_direct_map APIs. With increasing demand for kernel page table manipulations, for example "Direct Map Removal Support for guest_memfd" [1] and "mm: ASI direct map management" [2], it makes sense to have a generic core for kernel page table manipulation in mm/ with helpers in the architecture specific code. x86's implementation is the most versatile and most battle tested and as such it is the best base for the generic implementation. Pull change_page_attr() core that loops over memory ranges and updates their attributes in the page tables from arch/x86 to mm/. Add GENERIC_SET_MEMORY Kconfig option to let architectures opt-in for the generic implementation and enable it on x86. Define architecture hooks in include/linux/set_memory.h and rename x86 functions that implement them to match the generic declarations. At this point the generic implementation still includes x86 specifics, particularly pgd_lock. This will be abstracted in the upcoming changes. [1] https://lore.kernel.org/all/20260410151746.61150-1-kalyazin@amazon.com [2] https://lore.kernel.org/all/20250924-b4-asi-page-alloc-v1-0-2d861768041f@google.com/ No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/Kconfig | 1 + arch/x86/mm/pat/set_memory.c | 502 ++++--------------------------------------- include/linux/pgtable.h | 4 + include/linux/set_memory.h | 91 ++++++++ mm/Kconfig | 3 + mm/Makefile | 1 + mm/set_memory.c | 370 +++++++++++++++++++++++++++++++ 7 files changed, 513 insertions(+), 459 deletions(-) diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig index bdad90f210e4..e1f105e52d6a 100644 --- a/arch/x86/Kconfig +++ b/arch/x86/Kconfig @@ -336,6 +336,7 @@ config X86 select ARCH_SUPPORTS_SCHED_CLUSTER if SMP select ARCH_SUPPORTS_SCHED_MC if SMP select HAVE_SINGLE_FTRACE_DIRECT_OPS if X86_64 && DYNAMIC_FTRACE_WITH_DIRECT_CALLS + select GENERIC_SET_MEMORY config INSTRUCTION_DECODER def_bool y diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 56676d4cf510..5109139a2d31 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -38,35 +38,6 @@ /* * The current flushing context - we pass it instead of 5 arguments: */ -struct cpa_data { - unsigned long *vaddr; - pgd_t *pgd; - pgprot_t mask_set; - pgprot_t mask_clr; - unsigned long numpages; - unsigned long curpage; - unsigned long pfn; - unsigned int flags; - unsigned int force_split : 1, - force_static_prot : 1, - force_flush_all : 1; - struct page **pages; -}; - -/* - * context for checking for a split a large page and splitting it if required. - */ -struct cpa_split_data { - unsigned long address; - unsigned long psize; - unsigned long pmask; - pte_t *kpte; - struct ptdesc *ptdesc; - enum pgtable_level level; - bool nx; - bool rw; -}; - enum cpa_warn { CPA_CONFLICT, CPA_PROTECT, @@ -75,19 +46,6 @@ enum cpa_warn { static const int cpa_warn_level = CPA_PROTECT; -/* - * Serialize cpa() using cpa_lock so that we don't allow any other cpu, with - * stale large tlb entries, to change the page attribute in parallel to some - * other cpu splitting a large page entry along with changing the attribute. - */ -static DEFINE_SPINLOCK(cpa_lock); - -#define CPA_FLUSHTLB 1 -#define CPA_ARRAY 2 -#define CPA_PAGES_ARRAY 4 -#define CPA_NO_CHECK_ALIAS 8 /* Do not search for aliases */ -#define CPA_COLLAPSE 16 /* try to collapse large pages */ - static inline pgprot_t cachemode2pgprot(enum page_cache_mode pcm) { return __pgprot(cachemode2protval(pcm)); @@ -261,23 +219,6 @@ static inline unsigned long fix_addr(unsigned long addr) #endif } -static unsigned long __cpa_addr(struct cpa_data *cpa, unsigned long idx) -{ - if (cpa->flags & CPA_PAGES_ARRAY) { - struct page *page = cpa->pages[idx]; - - if (unlikely(PageHighMem(page))) - return 0; - - return (unsigned long)page_address(page); - } - - if (cpa->flags & CPA_ARRAY) - return cpa->vaddr[idx]; - - return *cpa->vaddr + idx * PAGE_SIZE; -} - /* * Flushing functions */ @@ -363,7 +304,7 @@ static void __cpa_flush_tlb(void *data) unsigned int i; for (i = 0; i < cpa->numpages; i++) - flush_tlb_one_kernel(fix_addr(__cpa_addr(cpa, i))); + flush_tlb_one_kernel(fix_addr(cpa_addr(cpa, i))); } static int collapse_large_pages(unsigned long addr, struct list_head *pgtables); @@ -378,10 +319,10 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa) if (cpa->flags & (CPA_PAGES_ARRAY | CPA_ARRAY)) { for (i = 0; i < cpa->numpages; i++) - collapsed += collapse_large_pages(__cpa_addr(cpa, i), + collapsed += collapse_large_pages(cpa_addr(cpa, i), &pgtables); } else { - addr = __cpa_addr(cpa, 0); + addr = cpa_addr(cpa, 0); start = addr & PMD_MASK; end = addr + PAGE_SIZE * cpa->numpages; @@ -421,7 +362,7 @@ static void cpa_flush(struct cpa_data *cpa, int cache) mb(); for (i = 0; i < cpa->numpages; i++) { - unsigned long addr = __cpa_addr(cpa, i); + unsigned long addr = cpa_addr(cpa, i); unsigned int level; pte_t *pte = lookup_address(addr, &level); @@ -667,104 +608,6 @@ static inline pgprot_t verify_rwx(pgprot_t old, pgprot_t new, unsigned long star return new; } -/* - * Lookup the page table entry for a virtual address in a specific pgd. - * Return a pointer to the entry (or NULL if the entry does not exist), - * the level of the entry, and the effective NX and RW bits of all - * page table levels. - */ -pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, - unsigned int *level, bool *nx, bool *rw) -{ - p4d_t *p4d; - pud_t *pud; - pmd_t *pmd; - - *level = PGTABLE_LEVEL_PGD; - *nx = false; - *rw = true; - - if (pgd_none(*pgd)) - return NULL; - - *level = PGTABLE_LEVEL_P4D; - *nx |= !pgd_exec(*pgd); - *rw &= !!(pgd_write(*pgd)); - - p4d = p4d_offset(pgd, address); - if (p4d_none(*p4d)) - return NULL; - - if (p4d_leaf(*p4d) || !p4d_present(*p4d)) - return (pte_t *)p4d; - - *level = PGTABLE_LEVEL_PUD; - *nx |= !p4d_exec(*p4d); - *rw &= !!(p4d_write(*p4d)); - - pud = pud_offset(p4d, address); - if (pud_none(*pud)) - return NULL; - - if (pud_leaf(*pud) || !pud_present(*pud)) - return (pte_t *)pud; - - *level = PGTABLE_LEVEL_PMD; - *nx |= !pud_exec(*pud); - *rw &= !!(pud_write(*pud)); - - pmd = pmd_offset(pud, address); - if (pmd_none(*pmd)) - return NULL; - - if (pmd_leaf(*pmd) || !pmd_present(*pmd)) - return (pte_t *)pmd; - - *level = PGTABLE_LEVEL_PTE; - *nx |= !pmd_exec(*pmd); - *rw &= !!(pmd_write(*pmd)); - - return pte_offset_kernel(pmd, address); -} - -/* - * Lookup the page table entry for a virtual address in a specific pgd. - * Return a pointer to the entry and the level of the mapping. - */ -pte_t *lookup_address_in_pgd(pgd_t *pgd, unsigned long address, - unsigned int *level) -{ - bool nx, rw; - - return lookup_address_in_pgd_attr(pgd, address, level, &nx, &rw); -} - -/* - * Lookup the page table entry for a virtual address. Return a pointer - * to the entry and the level of the mapping. - * - * Note: the function returns p4d, pud or pmd either when the entry is marked - * large or when the present bit is not set. Otherwise it returns NULL. - */ -pte_t *lookup_address(unsigned long address, unsigned int *level) -{ - return lookup_address_in_pgd(pgd_offset_k(address), address, level); -} -EXPORT_SYMBOL_GPL(lookup_address); - -static pte_t *_lookup_address_cpa(struct cpa_data *cpa, unsigned long address, - unsigned int *level, bool *nx, bool *rw) -{ - pgd_t *pgd; - - if (!cpa->pgd) - pgd = pgd_offset_k(address); - else - pgd = cpa->pgd + pgd_index(address); - - return lookup_address_in_pgd_attr(pgd, address, level, nx, rw); -} - /* * Lookup the PMD entry for a virtual address. Return a pointer to the entry * or NULL if not present. @@ -883,8 +726,8 @@ static pgprot_t pgprot_clear_protnone_bits(pgprot_t prot) return prot; } -static int __should_split_large_page(struct cpa_data *cpa, - struct cpa_split_data *sd) +int arch_should_split_large_page(struct cpa_data *cpa, + struct cpa_split_data *sd) { pgprot_t old_prot, new_prot, req_prot, chk_prot; unsigned long numpages, lpaddr, pfn, old_pfn; @@ -1015,31 +858,6 @@ static int __should_split_large_page(struct cpa_data *cpa, return 0; } -static int should_split_large_page(struct cpa_data *cpa, - struct cpa_split_data *sd) -{ - enum pgtable_level level = sd->level; - unsigned long address = sd->address; - unsigned long numpages, lpaddr; - - if (cpa->force_split) - return 1; - - sd->psize = pgtable_level_size(level); - sd->pmask = pgtable_level_mask(level); - - /* - * Calculate the number of pages, which fit into this large - * page starting at address: - */ - lpaddr = (address + sd->psize) & sd->pmask; - numpages = (lpaddr - address) >> PAGE_SHIFT; - if (numpages < cpa->numpages) - cpa->numpages = numpages; - - return __should_split_large_page(cpa, sd); -} - static void split_set_pte(struct cpa_data *cpa, pte_t *pte, unsigned long pfn, pgprot_t ref_prot, unsigned long address, unsigned long size) @@ -1076,7 +894,7 @@ static void split_set_pte(struct cpa_data *cpa, pte_t *pte, unsigned long pfn, set_pte(pte, pfn_pte(pfn, ref_prot)); } -static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) +int arch_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { unsigned long lpaddr, lpinc, ref_pfn, pfn, pfninc = 1; struct ptdesc *ptdesc = sd->ptdesc; @@ -1170,68 +988,6 @@ static int __split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) return 0; } -static int split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) -{ - return __split_large_page(cpa, sd); -} - -static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, - unsigned long address) -{ - struct cpa_split_data sd = { - .address = address, - .kpte = kpte, - }; - int do_split, ret = 1; - pte_t *tmp; - - spin_unlock(&cpa_lock); - sd.ptdesc = pagetable_alloc(GFP_KERNEL, 0); - spin_lock(&cpa_lock); - if (!sd.ptdesc) - return -ENOMEM; - - spin_lock(&pgd_lock); - - /* - * Check for races, another CPU might have split this page - * up already: - */ - tmp = _lookup_address_cpa(cpa, address, &sd.level, &sd.nx, &sd.rw); - if (tmp != kpte) - goto out_free_ptdesc; - - /* - * Check, whether we can keep the large page intact - * and just change the pte: - */ - do_split = should_split_large_page(cpa, &sd); - ret = do_split; - /* - * When the range fits into the existing large page, no split is - * required. - * should_split_large_page() updated the large page attributes and - * cpa->numpages and cpa->pfn. - */ - if (do_split <= 0) - goto out_free_ptdesc; - - /* - * We have to split the large page: - */ - ret = split_large_page(cpa, &sd); - if (ret) - goto out_free_ptdesc; - - spin_unlock(&pgd_lock); - return do_split; - -out_free_ptdesc: - spin_unlock(&pgd_lock); - pagetable_free(sd.ptdesc); - return ret; -} - static int collapse_pmd_page(pmd_t *pmd, unsigned long addr, struct list_head *pgtables) { @@ -1783,8 +1539,8 @@ static int populate_pgd(struct cpa_data *cpa, unsigned long addr) return 0; } -static int __cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, - int primary) +int arch_cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, + int primary) { if (cpa->pgd) { /* @@ -1828,75 +1584,45 @@ static int __cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, } } -static int __change_page_attr(struct cpa_data *cpa, int primary) +void arch_change_pte(struct cpa_data *cpa, unsigned long address, + pte_t *kpte, pte_t old_pte, bool nx, bool rw) { - unsigned long address; - int split_res; - unsigned int level; - pte_t *kpte, old_pte; - bool nx, rw; - - address = __cpa_addr(cpa, cpa->curpage); -repeat: - kpte = _lookup_address_cpa(cpa, address, &level, &nx, &rw); - if (!kpte) - return __cpa_process_fault(cpa, address, primary); - - old_pte = *kpte; - if (pte_none(old_pte)) - return __cpa_process_fault(cpa, address, primary); - - if (level == PGTABLE_LEVEL_PTE) { - pte_t new_pte; - pgprot_t old_prot = pte_pgprot(old_pte); - pgprot_t new_prot = pte_pgprot(old_pte); - unsigned long pfn = pte_pfn(old_pte); + pte_t new_pte; + pgprot_t old_prot = pte_pgprot(old_pte); + pgprot_t new_prot = pte_pgprot(old_pte); + unsigned long pfn = pte_pfn(old_pte); - pgprot_val(new_prot) &= ~pgprot_val(cpa->mask_clr); - pgprot_val(new_prot) |= pgprot_val(cpa->mask_set); + pgprot_val(new_prot) &= ~pgprot_val(cpa->mask_clr); + pgprot_val(new_prot) |= pgprot_val(cpa->mask_set); - cpa_inc_4k_install(); - /* Hand in lpsize = 0 to enforce the protection mechanism */ - new_prot = static_protections(new_prot, address, pfn, 1, 0, - CPA_PROTECT); + cpa_inc_4k_install(); + /* Hand in lpsize = 0 to enforce the protection mechanism */ + new_prot = static_protections(new_prot, address, pfn, 1, 0, + CPA_PROTECT); - new_prot = verify_rwx(old_prot, new_prot, address, pfn, 1, - nx, rw); + new_prot = verify_rwx(old_prot, new_prot, address, pfn, 1, nx, rw); - new_prot = pgprot_clear_protnone_bits(new_prot); + new_prot = pgprot_clear_protnone_bits(new_prot); - /* - * We need to keep the pfn from the existing PTE, - * after all we're only going to change its attributes - * not the memory it points to - */ - new_pte = pfn_pte(pfn, new_prot); - cpa->pfn = pfn; - /* - * Do we really change anything ? - */ - if (pte_val(old_pte) != pte_val(new_pte)) { - set_pte_atomic(kpte, new_pte); - cpa->flags |= CPA_FLUSHTLB; - } - cpa->numpages = 1; - return 0; + /* + * We need to keep the pfn from the existing PTE, after all we're only + * going to change its attributes not the memory it points to + */ + new_pte = pfn_pte(pfn, new_prot); + cpa->pfn = pfn; + /* + * Do we really change anything ? + */ + if (pte_val(old_pte) != pte_val(new_pte)) { + set_pte_atomic(kpte, new_pte); + cpa->flags |= CPA_FLUSHTLB; } - - /* Update large page or split it */ - split_res = cpa_handle_large_page(cpa, kpte, address); - if (split_res > 0) - goto repeat; - - return split_res; } -static int __change_page_attr_set_clr(struct cpa_data *cpa, int primary); - /* * Check the directmap and "high kernel map" 'aliases'. */ -static int cpa_process_alias(struct cpa_data *cpa) +int arch_cpa_process_alias(struct cpa_data *cpa) { struct cpa_data alias_cpa; unsigned long laddr = (unsigned long)__va(cpa->pfn << PAGE_SHIFT); @@ -1910,7 +1636,7 @@ static int cpa_process_alias(struct cpa_data *cpa) * No need to redo, when the primary call touched the direct * mapping already: */ - vaddr = __cpa_addr(cpa, cpa->curpage); + vaddr = cpa_addr(cpa, cpa->curpage); if (!(within(vaddr, PAGE_OFFSET, PAGE_OFFSET + (max_pfn_mapped << PAGE_SHIFT)))) { @@ -1968,167 +1694,25 @@ static int cpa_process_alias(struct cpa_data *cpa) return 0; } -static int __change_page_attr_set_clr(struct cpa_data *cpa, int primary) -{ - unsigned long numpages = cpa->numpages; - unsigned long rempages = numpages; - int ret = 0; - - /* - * No changes, easy! - */ - if (!(pgprot_val(cpa->mask_set) | pgprot_val(cpa->mask_clr)) && - !cpa->force_split) - return ret; - - while (rempages) { - /* - * Store the remaining nr of pages for the large page - * preservation check. - */ - cpa->numpages = rempages; - /* for array changes, we can't use large page */ - if (cpa->flags & (CPA_ARRAY | CPA_PAGES_ARRAY)) - cpa->numpages = 1; - - spin_lock(&cpa_lock); - ret = __change_page_attr(cpa, primary); - spin_unlock(&cpa_lock); - if (ret) - goto out; - - if (primary && !(cpa->flags & CPA_NO_CHECK_ALIAS) && - cpa_should_update_alias(__cpa_addr(cpa, cpa->curpage), cpa->pfn)) { - ret = cpa_process_alias(cpa); - if (ret) - goto out; - } - - /* - * Adjust the number of pages with the result of the - * CPA operation. Either a large page has been - * preserved or a single page update happened. - */ - BUG_ON(cpa->numpages > rempages || !cpa->numpages); - rempages -= cpa->numpages; - cpa->curpage += cpa->numpages; - } - -out: - /* Restore the original numpages */ - cpa->numpages = numpages; - return ret; -} - -static int change_page_attr_set_clr(unsigned long *addr, int numpages, - pgprot_t mask_set, pgprot_t mask_clr, - int force_split, int in_flag, - struct page **pages) +void arch_cpa_flush(struct cpa_data *cpa, int err) { - struct cpa_data cpa; - int ret, cache; - - memset(&cpa, 0, sizeof(cpa)); - - /* - * Check, if we are requested to set a not supported - * feature. Clearing non-supported features is OK. - */ - mask_set = canon_pgprot(mask_set); - - if (!pgprot_val(mask_set) && !pgprot_val(mask_clr) && !force_split) - return 0; - - /* Ensure we are PAGE_SIZE aligned */ - if (in_flag & CPA_ARRAY) { - int i; - for (i = 0; i < numpages; i++) { - if (addr[i] & ~PAGE_MASK) { - addr[i] &= PAGE_MASK; - WARN_ON_ONCE(1); - } - } - } else if (!(in_flag & CPA_PAGES_ARRAY)) { - /* - * in_flag of CPA_PAGES_ARRAY implies it is aligned. - * No need to check in that case - */ - if (*addr & ~PAGE_MASK) { - *addr &= PAGE_MASK; - /* - * People should not be passing in unaligned addresses: - */ - WARN_ON_ONCE(1); - } - } - - /* Must avoid aliasing mappings in the highmem code */ - kmap_flush_unused(); - - vm_unmap_aliases(); - - cpa.vaddr = addr; - cpa.pages = pages; - cpa.numpages = numpages; - cpa.mask_set = mask_set; - cpa.mask_clr = mask_clr; - cpa.flags = in_flag; - cpa.curpage = 0; - cpa.force_split = force_split; - - ret = __change_page_attr_set_clr(&cpa, 1); - - /* - * Check whether we really changed something: - */ - if (!(cpa.flags & CPA_FLUSHTLB)) - goto out; + int cache; /* * No need to flush, when we did not set any of the caching * attributes: */ - cache = !!pgprot2cachemode(mask_set); + cache = !!pgprot2cachemode(cpa->mask_set); /* * On error; flush everything to be sure. */ - if (ret) { + if (err) { cpa_flush_all(cache); - goto out; + return; } - cpa_flush(&cpa, cache); -out: - return ret; -} - -static inline int change_page_attr_set(unsigned long *addr, int numpages, - pgprot_t mask, int array) -{ - return change_page_attr_set_clr(addr, numpages, mask, __pgprot(0), 0, - (array ? CPA_ARRAY : 0), NULL); -} - -static inline int change_page_attr_clear(unsigned long *addr, int numpages, - pgprot_t mask, int array) -{ - return change_page_attr_set_clr(addr, numpages, __pgprot(0), mask, 0, - (array ? CPA_ARRAY : 0), NULL); -} - -static inline int cpa_set_pages_array(struct page **pages, int numpages, - pgprot_t mask) -{ - return change_page_attr_set_clr(NULL, numpages, mask, __pgprot(0), 0, - CPA_PAGES_ARRAY, pages); -} - -static inline int cpa_clear_pages_array(struct page **pages, int numpages, - pgprot_t mask) -{ - return change_page_attr_set_clr(NULL, numpages, __pgprot(0), mask, 0, - CPA_PAGES_ARRAY, pages); + cpa_flush(cpa, cache); } int _set_memory_uc(unsigned long addr, int numpages) diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h index 9eb00ad6d5d2..d1df8345852b 100644 --- a/include/linux/pgtable.h +++ b/include/linux/pgtable.h @@ -1782,6 +1782,10 @@ void arch_sync_kernel_mappings(unsigned long start, unsigned long end); * because these macros can be used even if CONFIG_MMU is not defined. */ +#ifndef canon_pgprot +#define canon_pgprot(prot) (prot) +#endif + #ifndef pgprot_nx #define pgprot_nx(prot) (prot) #endif diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h index 3030d9245f5a..86a7bef7cbca 100644 --- a/include/linux/set_memory.h +++ b/include/linux/set_memory.h @@ -84,4 +84,95 @@ static inline int set_memory_decrypted(unsigned long addr, int numpages) } #endif /* CONFIG_ARCH_HAS_MEM_ENCRYPT */ +#ifdef CONFIG_GENERIC_SET_MEMORY + +#include + +#define CPA_FLUSHTLB 1 +#define CPA_ARRAY 2 +#define CPA_PAGES_ARRAY 4 +#define CPA_NO_CHECK_ALIAS 8 /* Do not search for aliases */ +#define CPA_COLLAPSE 16 /* try to collapse large pages */ + +/* + * The current flushing context - we pass it instead of 5 arguments: + */ +struct cpa_data { + unsigned long *vaddr; + pgd_t *pgd; + pgprot_t mask_set; + pgprot_t mask_clr; + unsigned long numpages; + unsigned long curpage; + unsigned long pfn; + unsigned int flags; + unsigned int force_split : 1, + force_static_prot : 1, + force_flush_all : 1; + struct page **pages; +}; + +/* + * context for checking for a split a large page and splitting it if required. + */ +struct ptdesc; +struct cpa_split_data { + unsigned long address; + unsigned long psize; + unsigned long pmask; + pte_t *kpte; + struct ptdesc *ptdesc; + enum pgtable_level level; + bool nx; + bool rw; +}; + +unsigned long cpa_addr(struct cpa_data *cpa, unsigned long idx); + +int arch_cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, + int primary); +int arch_cpa_process_alias(struct cpa_data *cpa); +int arch_should_split_large_page(struct cpa_data *cpa, + struct cpa_split_data *sd); +int arch_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd); +void arch_change_pte(struct cpa_data *cpa, unsigned long address, + pte_t *kpte, pte_t old_pte, bool nx, bool rw); +void arch_cpa_flush(struct cpa_data *cpa, int err); + +int __change_page_attr_set_clr(struct cpa_data *cpa, int primary); +int change_page_attr_set_clr(unsigned long *addr, int numpages, + pgprot_t mask_set, pgprot_t mask_clr, + int force_split, int in_flag, + struct page **pages); + +static inline int change_page_attr_set(unsigned long *addr, int numpages, + pgprot_t mask, int array) +{ + return change_page_attr_set_clr(addr, numpages, mask, __pgprot(0), 0, + (array ? CPA_ARRAY : 0), NULL); +} + +static inline int change_page_attr_clear(unsigned long *addr, int numpages, + pgprot_t mask, int array) +{ + return change_page_attr_set_clr(addr, numpages, __pgprot(0), mask, 0, + (array ? CPA_ARRAY : 0), NULL); +} + +static inline int cpa_set_pages_array(struct page **pages, int numpages, + pgprot_t mask) +{ + return change_page_attr_set_clr(NULL, numpages, mask, __pgprot(0), 0, + CPA_PAGES_ARRAY, pages); +} + +static inline int cpa_clear_pages_array(struct page **pages, int numpages, + pgprot_t mask) +{ + return change_page_attr_set_clr(NULL, numpages, __pgprot(0), mask, 0, + CPA_PAGES_ARRAY, pages); +} + +#endif /* CONFIG_GENERIC_SET_MEMORY */ + #endif /* _LINUX_SET_MEMORY_H_ */ diff --git a/mm/Kconfig b/mm/Kconfig index 9e0ca4824905..541edd1e11b6 100644 --- a/mm/Kconfig +++ b/mm/Kconfig @@ -1509,6 +1509,9 @@ config LAZY_MMU_MODE_KUNIT_TEST If unsure, say N. +config GENERIC_SET_MEMORY + bool + source "mm/damon/Kconfig" endmenu diff --git a/mm/Makefile b/mm/Makefile index eff9f9e7e061..e3f6c39c2ba9 100644 --- a/mm/Makefile +++ b/mm/Makefile @@ -147,3 +147,4 @@ obj-$(CONFIG_SHRINKER_DEBUG) += shrinker_debug.o obj-$(CONFIG_EXECMEM) += execmem.o obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o +obj-$(CONFIG_GENERIC_SET_MEMORY) += set_memory.o diff --git a/mm/set_memory.c b/mm/set_memory.c new file mode 100644 index 000000000000..712264de8e9b --- /dev/null +++ b/mm/set_memory.c @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include +#include +#include +#include + +#include + +/* + * Serialize cpa() so that we don't allow any other cpu, with stale large tlb + * entries, to change the page attribute in parallel to some other cpu + * splitting a large page entry along with changing the attribute. + */ +static DEFINE_SPINLOCK(cpa_lock); + +unsigned long cpa_addr(struct cpa_data *cpa, unsigned long idx) +{ + if (cpa->flags & CPA_PAGES_ARRAY) { + struct page *page = cpa->pages[idx]; + + if (unlikely(PageHighMem(page))) + return 0; + + return (unsigned long)page_address(page); + } + + if (cpa->flags & CPA_ARRAY) + return cpa->vaddr[idx]; + + return *cpa->vaddr + idx * PAGE_SIZE; +} + +/* + * Lookup the page table entry for a virtual address in a specific pgd. + * Return a pointer to the entry (or NULL if the entry does not exist), + * the level of the entry, and the effective NX and RW bits of all + * page table levels. + */ +pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, + unsigned int *level, bool *nx, bool *rw) +{ + p4d_t *p4d; + pud_t *pud; + pmd_t *pmd; + + *level = PGTABLE_LEVEL_PGD; + *nx = false; + *rw = true; + + if (pgd_none(*pgd)) + return NULL; + + *level = PGTABLE_LEVEL_P4D; + *nx |= !pgd_exec(*pgd); + *rw &= !!(pgd_write(*pgd)); + + p4d = p4d_offset(pgd, address); + if (p4d_none(*p4d)) + return NULL; + + if (p4d_leaf(*p4d) || !p4d_present(*p4d)) + return (pte_t *)p4d; + + *level = PGTABLE_LEVEL_PUD; + *nx |= !p4d_exec(*p4d); + *rw &= !!(p4d_write(*p4d)); + + pud = pud_offset(p4d, address); + if (pud_none(*pud)) + return NULL; + + if (pud_leaf(*pud) || !pud_present(*pud)) + return (pte_t *)pud; + + *level = PGTABLE_LEVEL_PMD; + *nx |= !pud_exec(*pud); + *rw &= !!(pud_write(*pud)); + + pmd = pmd_offset(pud, address); + if (pmd_none(*pmd)) + return NULL; + + if (pmd_leaf(*pmd) || !pmd_present(*pmd)) + return (pte_t *)pmd; + + *level = PGTABLE_LEVEL_PTE; + *nx |= !pmd_exec(*pmd); + *rw &= !!(pmd_write(*pmd)); + + return pte_offset_kernel(pmd, address); +} + +/* + * Lookup the page table entry for a virtual address in a specific pgd. + * Return a pointer to the entry and the level of the mapping. + */ +pte_t *lookup_address_in_pgd(pgd_t *pgd, unsigned long address, + unsigned int *level) +{ + bool nx, rw; + + return lookup_address_in_pgd_attr(pgd, address, level, &nx, &rw); +} + +/* + * Lookup the page table entry for a virtual address. Return a pointer + * to the entry and the level of the mapping. + * + * Note: the function returns p4d, pud or pmd either when the entry is marked + * large or when the present bit is not set. Otherwise it returns NULL. + */ +pte_t *lookup_address(unsigned long address, unsigned int *level) +{ + return lookup_address_in_pgd(pgd_offset_k(address), address, level); +} +EXPORT_SYMBOL_GPL(lookup_address); + +static pte_t *_lookup_address_cpa(struct cpa_data *cpa, unsigned long address, + unsigned int *level, bool *nx, bool *rw) +{ + pgd_t *pgd; + + if (!cpa->pgd) + pgd = pgd_offset_k(address); + else + pgd = cpa->pgd + pgd_index(address); + + return lookup_address_in_pgd_attr(pgd, address, level, nx, rw); +} + +static int should_split_large_page(struct cpa_data *cpa, + struct cpa_split_data *sd) +{ + enum pgtable_level level = sd->level; + unsigned long address = sd->address; + unsigned long numpages, lpaddr; + + if (cpa->force_split) + return 1; + + sd->psize = pgtable_level_size(level); + sd->pmask = pgtable_level_mask(level); + + /* + * Calculate the number of pages, which fit into this large + * page starting at address: + */ + lpaddr = (address + sd->psize) & sd->pmask; + numpages = (lpaddr - address) >> PAGE_SHIFT; + if (numpages < cpa->numpages) + cpa->numpages = numpages; + + return arch_should_split_large_page(cpa, sd); +} + +static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, + unsigned long address) +{ + struct cpa_split_data sd = { + .address = address, + .kpte = kpte, + }; + int do_split, ret = 1; + pte_t *tmp; + + spin_unlock(&cpa_lock); + sd.ptdesc = pagetable_alloc(GFP_KERNEL, 0); + spin_lock(&cpa_lock); + if (!sd.ptdesc) + return -ENOMEM; + + spin_lock(&pgd_lock); + + /* + * Check for races, another CPU might have split this page + * up already: + */ + tmp = _lookup_address_cpa(cpa, address, &sd.level, &sd.nx, &sd.rw); + if (tmp != kpte) + goto out_free_ptdesc; + + /* + * Check, whether we can keep the large page intact + * and just change the pte: + */ + do_split = should_split_large_page(cpa, &sd); + ret = do_split; + /* + * When the range fits into the existing large page, no split is + * required. + * should_split_large_page() updated the large page attributes and + * cpa->numpages and cpa->pfn. + */ + if (do_split <= 0) + goto out_free_ptdesc; + + /* + * We have to split the large page: + */ + ret = arch_split_large_page(cpa, &sd); + if (ret) + goto out_free_ptdesc; + + spin_unlock(&pgd_lock); + return do_split; + +out_free_ptdesc: + spin_unlock(&pgd_lock); + pagetable_free(sd.ptdesc); + return ret; +} + +static int __change_page_attr(struct cpa_data *cpa, int primary) +{ + unsigned long address; + int split_res; + unsigned int level; + pte_t *kpte, old_pte; + bool nx, rw; + + address = cpa_addr(cpa, cpa->curpage); +repeat: + kpte = _lookup_address_cpa(cpa, address, &level, &nx, &rw); + if (!kpte) + return arch_cpa_process_fault(cpa, address, primary); + + old_pte = *kpte; + if (pte_none(old_pte)) + return arch_cpa_process_fault(cpa, address, primary); + + if (level == PGTABLE_LEVEL_PTE) { + arch_change_pte(cpa, address, kpte, old_pte, nx, rw); + cpa->numpages = 1; + return 0; + } + + /* + * The address maps a large page. Try to keep it, otherwise split it + * up and retry on the freshly installed smaller mapping. + */ + split_res = cpa_handle_large_page(cpa, kpte, address); + if (split_res > 0) + goto repeat; + + return split_res; +} + +int __change_page_attr_set_clr(struct cpa_data *cpa, int primary) +{ + unsigned long numpages = cpa->numpages; + unsigned long rempages = numpages; + int ret = 0; + + /* + * No changes, easy! + */ + if (!(pgprot_val(cpa->mask_set) | pgprot_val(cpa->mask_clr)) && + !cpa->force_split) + return ret; + + while (rempages) { + /* + * Store the remaining nr of pages for the large page + * preservation check. + */ + cpa->numpages = rempages; + /* for array changes, we can't use large page */ + if (cpa->flags & (CPA_ARRAY | CPA_PAGES_ARRAY)) + cpa->numpages = 1; + + spin_lock(&cpa_lock); + ret = __change_page_attr(cpa, primary); + spin_unlock(&cpa_lock); + if (ret) + goto out; + + if (primary && !(cpa->flags & CPA_NO_CHECK_ALIAS) && + cpa_should_update_alias(cpa_addr(cpa, cpa->curpage), cpa->pfn)) { + ret = arch_cpa_process_alias(cpa); + if (ret) + goto out; + } + + /* + * Adjust the number of pages with the result of the + * CPA operation. Either a large page has been + * preserved or a single page update happened. + */ + BUG_ON(cpa->numpages > rempages || !cpa->numpages); + rempages -= cpa->numpages; + cpa->curpage += cpa->numpages; + } + +out: + /* Restore the original numpages */ + cpa->numpages = numpages; + return ret; +} + +int change_page_attr_set_clr(unsigned long *addr, int numpages, + pgprot_t mask_set, pgprot_t mask_clr, + int force_split, int in_flag, + struct page **pages) +{ + struct cpa_data cpa; + int err; + + memset(&cpa, 0, sizeof(cpa)); + + /* + * Check, if we are requested to set a not supported + * feature. Clearing non-supported features is OK. + */ + mask_set = canon_pgprot(mask_set); + + if (!pgprot_val(mask_set) && !pgprot_val(mask_clr) && !force_split) + return 0; + + /* Ensure we are PAGE_SIZE aligned */ + if (in_flag & CPA_ARRAY) { + int i; + + for (i = 0; i < numpages; i++) { + if (addr[i] & ~PAGE_MASK) { + addr[i] &= PAGE_MASK; + WARN_ON_ONCE(1); + } + } + } else if (!(in_flag & CPA_PAGES_ARRAY)) { + /* + * in_flag of CPA_PAGES_ARRAY implies it is aligned. + * No need to check in that case + */ + if (*addr & ~PAGE_MASK) { + *addr &= PAGE_MASK; + /* + * People should not be passing in unaligned addresses: + */ + WARN_ON_ONCE(1); + } + } + + /* Must avoid aliasing mappings in the highmem code */ + kmap_flush_unused(); + + vm_unmap_aliases(); + + cpa.vaddr = addr; + cpa.pages = pages; + cpa.numpages = numpages; + cpa.mask_set = mask_set; + cpa.mask_clr = mask_clr; + cpa.flags = in_flag; + cpa.curpage = 0; + cpa.force_split = force_split; + + err = __change_page_attr_set_clr(&cpa, 1); + + /* + * Check whether we really changed something: + */ + if (!(cpa.flags & CPA_FLUSHTLB)) + goto out; + + arch_cpa_flush(&cpa, err); + +out: + return err; +} -- 2.53.0 x86 requires taking pgd_lock to serialize large page splits against parallel page table operations, for instance such as arch_sync_kernel_mappings() and pgd allocation and freeing. Add arch_lock() and arch_unlock() static inlines that replace direct locking/unlocking of the pgd_lock in CPA core. Since only x86 needs such lock, keep things simple and use #ifdef CONFIG_X86 rather than creating an overridable infrastructure. No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- mm/set_memory.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/mm/set_memory.c b/mm/set_memory.c index 712264de8e9b..d2846804e4e6 100644 --- a/mm/set_memory.c +++ b/mm/set_memory.c @@ -14,6 +14,20 @@ */ static DEFINE_SPINLOCK(cpa_lock); +static inline void arch_lock(void) +{ +#ifdef CONFIG_X86 + spin_lock(&pgd_lock); +#endif +} + +static inline void arch_unlock(void) +{ +#ifdef CONFIG_X86 + spin_unlock(&pgd_lock); +#endif +} + unsigned long cpa_addr(struct cpa_data *cpa, unsigned long idx) { if (cpa->flags & CPA_PAGES_ARRAY) { @@ -170,7 +184,7 @@ static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, if (!sd.ptdesc) return -ENOMEM; - spin_lock(&pgd_lock); + arch_lock(); /* * Check for races, another CPU might have split this page @@ -202,11 +216,11 @@ static int cpa_handle_large_page(struct cpa_data *cpa, pte_t *kpte, if (ret) goto out_free_ptdesc; - spin_unlock(&pgd_lock); + arch_unlock(); return do_split; out_free_ptdesc: - spin_unlock(&pgd_lock); + arch_unlock(); pagetable_free(sd.ptdesc); return ret; } -- 2.53.0 As a preparation for enabling generic set_memory support in riscv, replace direct dereferences of the page table pointers with pXdp_get() accessors. No functional change intended. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- mm/set_memory.c | 56 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/mm/set_memory.c b/mm/set_memory.c index d2846804e4e6..1a7e63512981 100644 --- a/mm/set_memory.c +++ b/mm/set_memory.c @@ -51,58 +51,62 @@ unsigned long cpa_addr(struct cpa_data *cpa, unsigned long idx) * the level of the entry, and the effective NX and RW bits of all * page table levels. */ -pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, +pte_t *lookup_address_in_pgd_attr(pgd_t *pgdp, unsigned long address, unsigned int *level, bool *nx, bool *rw) { - p4d_t *p4d; - pud_t *pud; - pmd_t *pmd; + pgd_t pgd = pgdp_get(pgdp); + p4d_t *p4dp, p4d; + pud_t *pudp, pud; + pmd_t *pmdp, pmd; *level = PGTABLE_LEVEL_PGD; *nx = false; *rw = true; - if (pgd_none(*pgd)) + if (pgd_none(pgd)) return NULL; *level = PGTABLE_LEVEL_P4D; - *nx |= !pgd_exec(*pgd); - *rw &= !!(pgd_write(*pgd)); + *nx |= !pgd_exec(pgd); + *rw &= !!(pgd_write(pgd)); - p4d = p4d_offset(pgd, address); - if (p4d_none(*p4d)) + p4dp = p4d_offset(pgdp, address); + p4d = p4dp_get(p4dp); + if (p4d_none(p4d)) return NULL; - if (p4d_leaf(*p4d) || !p4d_present(*p4d)) - return (pte_t *)p4d; + if (p4d_leaf(p4d) || !p4d_present(p4d)) + return (pte_t *)p4dp; *level = PGTABLE_LEVEL_PUD; - *nx |= !p4d_exec(*p4d); - *rw &= !!(p4d_write(*p4d)); + *nx |= !p4d_exec(p4d); + *rw &= !!(p4d_write(p4d)); - pud = pud_offset(p4d, address); - if (pud_none(*pud)) + pudp = pud_offset(p4dp, address); + pud = pudp_get(pudp); + if (pud_none(pud)) return NULL; - if (pud_leaf(*pud) || !pud_present(*pud)) - return (pte_t *)pud; + if (pud_leaf(pud) || !pud_present(pud)) + return (pte_t *)pudp; *level = PGTABLE_LEVEL_PMD; - *nx |= !pud_exec(*pud); - *rw &= !!(pud_write(*pud)); + *nx |= !pud_exec(pud); + *rw &= !!(pud_write(pud)); - pmd = pmd_offset(pud, address); - if (pmd_none(*pmd)) + pmdp = pmd_offset(pudp, address); + pmd = pmdp_get(pmdp); + if (pmd_none(pmd)) return NULL; - if (pmd_leaf(*pmd) || !pmd_present(*pmd)) - return (pte_t *)pmd; + if (pmd_leaf(pmd) || !pmd_present(pmd)) + return (pte_t *)pmdp; *level = PGTABLE_LEVEL_PTE; - *nx |= !pmd_exec(*pmd); - *rw &= !!(pmd_write(*pmd)); + *nx |= !pmd_exec(pmd); + *rw &= !!(pmd_write(pmd)); - return pte_offset_kernel(pmd, address); + return pte_offset_kernel(pmdp, address); } /* -- 2.53.0 x86 has a nice test suite for set_memory APIs and CPA machinery. Now that core CPA machinery is a part of the generic code let's make the test also available as the part of the common code: * move arch/x86/mm/pat/cpa-test.c to mm/ * keep it included in set_memory.c rather than a separate compilation unit. * adjust the code to use pXd accessors instead of hardcoded x86 specific bits * replace max_pfn_mapped used as an upper bound with test local max_test_pfn derived from high_memory. Signed-off-by: Mike Rapoport (Microsoft) --- arch/x86/Kconfig.debug | 6 ---- arch/x86/mm/pat/set_memory.c | 8 ----- mm/Kconfig.debug | 7 ++++ {arch/x86/mm/pat => mm}/cpa-test.c | 66 +++++++++++++++++++------------------- mm/set_memory.c | 8 +++++ 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/arch/x86/Kconfig.debug b/arch/x86/Kconfig.debug index c95c3aaadf97..314b72aad6ff 100644 --- a/arch/x86/Kconfig.debug +++ b/arch/x86/Kconfig.debug @@ -157,12 +157,6 @@ config DEBUG_BOOT_PARAMS help This option will cause struct boot_params to be exported via debugfs. -config CPA_DEBUG - bool "CPA self-test code" - depends on DEBUG_KERNEL - help - Do change_page_attr() self-tests every 30 seconds. - config DEBUG_ENTRY bool "Debug low-level entry code" depends on DEBUG_KERNEL diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c index 5109139a2d31..c460121c96b9 100644 --- a/arch/x86/mm/pat/set_memory.c +++ b/arch/x86/mm/pat/set_memory.c @@ -2324,11 +2324,3 @@ int __init kernel_unmap_pages_in_pgd(pgd_t *pgd, unsigned long address, return retval; } - -/* - * The testcases use internal knowledge of the implementation that shouldn't - * be exposed to the rest of the kernel. Include these directly here. - */ -#ifdef CONFIG_CPA_DEBUG -#include "cpa-test.c" -#endif diff --git a/mm/Kconfig.debug b/mm/Kconfig.debug index 91b3e027b753..83cfef6b2d8c 100644 --- a/mm/Kconfig.debug +++ b/mm/Kconfig.debug @@ -320,3 +320,10 @@ config PER_VMA_LOCK_STATS overhead in the page fault path. If in doubt, say N. + +config CPA_DEBUG + bool "CPA self-test code" + depends on GENERIC_SET_MEMORY + depends on DEBUG_KERNEL + help + Do change_page_attr() self-tests every 30 seconds. diff --git a/arch/x86/mm/pat/cpa-test.c b/mm/cpa-test.c similarity index 80% rename from arch/x86/mm/pat/cpa-test.c rename to mm/cpa-test.c index ad3c1feec990..63c3463a5a7d 100644 --- a/arch/x86/mm/pat/cpa-test.c +++ b/mm/cpa-test.c @@ -13,32 +13,24 @@ #include #include -#include -#include - /* * Only print the results of the first pass: */ static __read_mostly int print = 1; +static unsigned long max_test_pfn; enum { NTEST = 3 * 100, NPAGES = 100, -#ifdef CONFIG_X86_64 - LPS = (1 << PMD_SHIFT), -#elif defined(CONFIG_X86_PAE) - LPS = (1 << PMD_SHIFT), -#else - LPS = (1 << 22), -#endif - GPS = (1<<30) + LPS = PMD_SIZE, + GPS = PUD_SIZE }; -#define PAGE_CPA_TEST __pgprot(_PAGE_CPA_TEST) +#define PAGE_CPA_TEST __pgprot(_PAGE_SPECIAL) static int pte_testbit(pte_t pte) { - return pte_flags(pte) & _PAGE_SOFTW1; + return pte_special(pte); } struct split_state { @@ -54,10 +46,10 @@ static int print_split(struct split_state *s) s->lpg = s->gpg = s->spg = s->exec = 0; s->min_exec = ~0UL; s->max_exec = 0; - for (i = 0; i < max_pfn_mapped; ) { + for (i = 0; i < max_test_pfn; ) { unsigned long addr = (unsigned long)__va(i << PAGE_SHIFT); unsigned int level; - pte_t *pte; + pte_t *pte, pteval; pte = lookup_address(addr, &level); if (!pte) { @@ -65,15 +57,16 @@ static int print_split(struct split_state *s) i++; continue; } + pteval = ptep_get(pte); - if (level == PG_LEVEL_1G && sizeof(long) == 8) { + if (level == PGTABLE_LEVEL_PUD && sizeof(long) == 8) { s->gpg++; i += GPS/PAGE_SIZE; - } else if (level == PG_LEVEL_2M) { - if ((pte_val(*pte) & _PAGE_PRESENT) && !(pte_val(*pte) & _PAGE_PSE)) { + } else if (level == PGTABLE_LEVEL_PMD) { + if (pte_present(pteval) && !pte_huge(pteval)) { printk(KERN_ERR - "%lx level %d but not PSE %Lx\n", - addr, level, (u64)pte_val(*pte)); + "%lx level %d but not leaf %Lx\n", + addr, level, (u64)pte_val(pteval)); err = 1; } s->lpg++; @@ -82,7 +75,7 @@ static int print_split(struct split_state *s) s->spg++; i++; } - if (!(pte_val(*pte) & _PAGE_NX)) { + if (pte_exec(pteval)) { s->exec++; if (addr < s->min_exec) s->min_exec = addr; @@ -100,8 +93,8 @@ static int print_split(struct split_state *s) expected = (s->gpg*GPS + s->lpg*LPS)/PAGE_SIZE + s->spg + missed; if (expected != i) { - printk(KERN_ERR "CPA max_pfn_mapped %lu but expected %lu\n", - max_pfn_mapped, expected); + printk(KERN_ERR "CPA max_test_pfn %lu but expected %lu\n", + max_test_pfn, expected); return 1; } return err; @@ -118,7 +111,7 @@ static int pageattr_test(void) { struct split_state sa, sb, sc; unsigned long *bm; - pte_t *pte, pte0; + pte_t *pte, pte0, pte_val; int failed = 0; unsigned int level; int i, k; @@ -127,7 +120,7 @@ static int pageattr_test(void) if (print) printk(KERN_INFO "CPA self-test:\n"); - bm = vzalloc((max_pfn_mapped + 7) / 8); + bm = vzalloc((max_test_pfn + 7) / 8); if (!bm) { printk(KERN_ERR "CPA Cannot vmalloc bitmap\n"); return -ENOMEM; @@ -136,11 +129,11 @@ static int pageattr_test(void) failed += print_split(&sa); for (i = 0; i < NTEST; i++) { - unsigned long pfn = get_random_u32_below(max_pfn_mapped); + unsigned long pfn = get_random_u32_below(max_test_pfn); addr[i] = (unsigned long)__va(pfn << PAGE_SHIFT); len[i] = get_random_u32_below(NPAGES); - len[i] = min_t(unsigned long, len[i], max_pfn_mapped - pfn - 1); + len[i] = min_t(unsigned long, len[i], max_test_pfn - pfn - 1); if (len[i] == 0) len[i] = 1; @@ -150,15 +143,20 @@ static int pageattr_test(void) for (k = 0; k < len[i]; k++) { pte = lookup_address(addr[i] + k*PAGE_SIZE, &level); - if (!pte || pgprot_val(pte_pgprot(*pte)) == 0 || - !(pte_val(*pte) & _PAGE_PRESENT)) { + if (!pte) { + addr[i] = 0; + break; + } + pte_val = ptep_get(pte); + if (pgprot_val(pte_pgprot(pte_val)) == 0 || + !pte_present(pte_val)) { addr[i] = 0; break; } if (k == 0) { - pte0 = *pte; + pte0 = pte_val; } else { - if (pgprot_val(pte_pgprot(*pte)) != + if (pgprot_val(pte_pgprot(pte_val)) != pgprot_val(pte_pgprot(pte0))) { len[i] = k; break; @@ -198,12 +196,12 @@ static int pageattr_test(void) } pte = lookup_address(addr[i], &level); - if (!pte || !pte_testbit(*pte) || pte_huge(*pte)) { + if (!pte || !pte_testbit(*pte) || level != PGTABLE_LEVEL_PTE) { printk(KERN_ERR "CPA %lx: bad pte %Lx\n", addr[i], pte ? (u64)pte_val(*pte) : 0ULL); failed++; } - if (level != PG_LEVEL_4K) { + if (level != PGTABLE_LEVEL_PTE) { printk(KERN_ERR "CPA %lx: unexpected level %d\n", addr[i], level); failed++; @@ -266,6 +264,8 @@ static int start_pageattr_test(void) { struct task_struct *p; + max_test_pfn = PFN_DOWN(__pa(high_memory - 1)); + p = kthread_create(do_pageattr_test, NULL, "pageattr-test"); if (!IS_ERR(p)) wake_up_process(p); diff --git a/mm/set_memory.c b/mm/set_memory.c index 1a7e63512981..744405efcc34 100644 --- a/mm/set_memory.c +++ b/mm/set_memory.c @@ -386,3 +386,11 @@ int change_page_attr_set_clr(unsigned long *addr, int numpages, out: return err; } + +/* + * The testcases use internal knowledge of the implementation that shouldn't + * be exposed to the rest of the kernel. Include these directly here. + */ +#ifdef CONFIG_CPA_DEBUG +#include "cpa-test.c" +#endif -- 2.53.0 Replace the pageattr_ops based page table walk in arch/riscv/mm/pageattr.c with the common change_page_attr() loop, implement the required architecture hooks and opt-in for CONFIG_GENERIC_SET_MEMORY. Instead if pre-splitting large pages with split_linear_mapping() use riscv-specific implementation of arch_should_split_large_page() and arch_split_large_page(). Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/riscv/Kconfig | 1 + arch/riscv/include/asm/pgtable.h | 36 +++ arch/riscv/include/asm/set_memory.h | 7 + arch/riscv/mm/pageattr.c | 465 +++++++++++------------------------- include/linux/set_memory.h | 6 + 5 files changed, 183 insertions(+), 332 deletions(-) diff --git a/arch/riscv/Kconfig b/arch/riscv/Kconfig index 3f0a647218e4..5d069bf290ce 100644 --- a/arch/riscv/Kconfig +++ b/arch/riscv/Kconfig @@ -48,6 +48,7 @@ config RISCV select ARCH_HAS_PTE_SPECIAL select ARCH_HAS_SET_DIRECT_MAP if MMU select ARCH_HAS_SET_MEMORY if MMU + select GENERIC_SET_MEMORY if MMU select ARCH_HAS_STRICT_KERNEL_RWX if MMU select ARCH_HAS_STRICT_MODULE_RWX if MMU select ARCH_HAS_SYNC_CORE_BEFORE_USERMODE diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h index 5d5756bda82e..027b3e7e4f17 100644 --- a/arch/riscv/include/asm/pgtable.h +++ b/arch/riscv/include/asm/pgtable.h @@ -819,6 +819,42 @@ static inline int pud_write(pud_t pud) return pte_write(pud_pte(pud)); } +#define pgd_write pgd_write +static inline int pgd_write(pgd_t pgd) +{ + return pgd_val(pgd) & _PAGE_WRITE; +} + +#define p4d_write p4d_write +static inline int p4d_write(p4d_t p4d) +{ + return p4d_val(p4d) & _PAGE_WRITE; +} + +#define pgd_exec pgd_exec +static inline int pgd_exec(pgd_t pgd) +{ + return !!(pgd_val(pgd) & _PAGE_EXEC); +} + +#define p4d_exec p4d_exec +static inline int p4d_exec(p4d_t p4d) +{ + return !!(p4d_val(p4d) & _PAGE_EXEC); +} + +#define pud_exec pud_exec +static inline int pud_exec(pud_t pud) +{ + return !!(pud_val(pud) & _PAGE_EXEC); +} + +#define pmd_exec pmd_exec +static inline int pmd_exec(pmd_t pmd) +{ + return !!(pmd_val(pmd) & _PAGE_EXEC); +} + #define pmd_dirty pmd_dirty static inline int pmd_dirty(pmd_t pmd) { diff --git a/arch/riscv/include/asm/set_memory.h b/arch/riscv/include/asm/set_memory.h index ef59e1716a2c..4ba46a331034 100644 --- a/arch/riscv/include/asm/set_memory.h +++ b/arch/riscv/include/asm/set_memory.h @@ -7,6 +7,8 @@ #define _ASM_RISCV_SET_MEMORY_H #ifndef __ASSEMBLER__ +#include +#include /* * Functions to change memory attributes. */ @@ -26,6 +28,11 @@ static __always_inline int set_kernel_memory(char *startp, char *endp, return set_memory(start, num_pages); } + +static inline bool cpa_should_update_alias(unsigned long vaddr, unsigned long pfn) +{ + return !is_linear_mapping(vaddr); +} #else static inline int set_memory_ro(unsigned long addr, int numpages) { return 0; } static inline int set_memory_rw(unsigned long addr, int numpages) { return 0; } diff --git a/arch/riscv/mm/pageattr.c b/arch/riscv/mm/pageattr.c index 3f76db3d2769..677f42a6750d 100644 --- a/arch/riscv/mm/pageattr.c +++ b/arch/riscv/mm/pageattr.c @@ -3,391 +3,217 @@ * Copyright (C) 2019 SiFive */ -#include +#include #include -#include +#include #include -#include #include -struct pageattr_masks { - pgprot_t set_mask; - pgprot_t clear_mask; -}; - -static unsigned long set_pageattr_masks(unsigned long val, struct mm_walk *walk) +int arch_should_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - struct pageattr_masks *masks = walk->private; - unsigned long new_val = val; - - new_val &= ~(pgprot_val(masks->clear_mask)); - new_val |= (pgprot_val(masks->set_mask)); + pte_t old = ptep_get(sd->kpte); + pgprot_t old_prot = __pgprot(pte_val(old) & ~_PAGE_PFN_MASK); + pgprot_t new_prot = old_prot; + unsigned long old_pfn = pte_pfn(old); + unsigned long lpaddr, numpages; - return new_val; -} - -static int pageattr_p4d_entry(p4d_t *p4d, unsigned long addr, - unsigned long next, struct mm_walk *walk) -{ - p4d_t val = p4dp_get(p4d); + pgprot_val(new_prot) &= ~pgprot_val(cpa->mask_clr); + pgprot_val(new_prot) |= pgprot_val(cpa->mask_set); - if (p4d_leaf(val)) { - val = __p4d(set_pageattr_masks(p4d_val(val), walk)); - set_p4d(p4d, val); - } + /* + * Record the pfn mapped at @address so the alias check can locate the + * matching linear map entry. + */ + cpa->pfn = old_pfn + ((sd->address & (sd->psize - 1)) >> PAGE_SHIFT); - return 0; -} + /* If the protections do not change, keep the large page intact. */ + if (pgprot_val(new_prot) == pgprot_val(old_prot)) + return 0; -static int pageattr_pud_entry(pud_t *pud, unsigned long addr, - unsigned long next, struct mm_walk *walk) -{ - pud_t val = pudp_get(pud); + /* If the request does not cover the whole large page, split it. */ + lpaddr = sd->address & sd->pmask; + numpages = sd->psize >> PAGE_SHIFT; + if (sd->address != lpaddr || cpa->numpages != numpages) + return 1; - if (pud_leaf(val)) { - val = __pud(set_pageattr_masks(pud_val(val), walk)); - set_pud(pud, val); - } + /* The whole large page is covered: update it in place. */ + set_pte(sd->kpte, pfn_pte(old_pfn, new_prot)); + cpa->flags |= CPA_FLUSHTLB; return 0; } -static int pageattr_pmd_entry(pmd_t *pmd, unsigned long addr, - unsigned long next, struct mm_walk *walk) +static void split_set_ptes(pte_t *ptep, unsigned long pfn, unsigned long pfninc, + pgprot_t prot) { - pmd_t val = pmdp_get(pmd); - - if (pmd_leaf(val)) { - val = __pmd(set_pageattr_masks(pmd_val(val), walk)); - set_pmd(pmd, val); - } + unsigned int i; - return 0; + for (i = 0; i < PTRS_PER_PTE; ++i, ++ptep, pfn += pfninc) + set_pte(ptep, pfn_pte(pfn, prot)); } -static int pageattr_pte_entry(pte_t *pte, unsigned long addr, - unsigned long next, struct mm_walk *walk) +int arch_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - pte_t val = ptep_get(pte); - - val = __pte(set_pageattr_masks(pte_val(val), walk)); - set_pte(pte, val); - - return 0; -} - -static int pageattr_pte_hole(unsigned long addr, unsigned long next, - int depth, struct mm_walk *walk) -{ - /* Nothing to do here */ - return 0; -} - -static const struct mm_walk_ops pageattr_ops = { - .p4d_entry = pageattr_p4d_entry, - .pud_entry = pageattr_pud_entry, - .pmd_entry = pageattr_pmd_entry, - .pte_entry = pageattr_pte_entry, - .pte_hole = pageattr_pte_hole, - .walk_lock = PGWALK_RDLOCK, -}; - #ifdef CONFIG_64BIT -static int __split_linear_mapping_pmd(pud_t *pudp, - unsigned long vaddr, unsigned long end) -{ - pmd_t *pmdp; - unsigned long next; - - pmdp = pmd_offset(pudp, vaddr); - - do { - next = pmd_addr_end(vaddr, end); - - if (next - vaddr >= PMD_SIZE && - vaddr <= (vaddr & PMD_MASK) && end >= next) - continue; - - if (pmd_leaf(pmdp_get(pmdp))) { - struct page *pte_page; - unsigned long pfn = _pmd_pfn(pmdp_get(pmdp)); - pgprot_t prot = __pgprot(pmd_val(pmdp_get(pmdp)) & ~_PAGE_PFN_MASK); - pte_t *ptep_new; - int i; - - pte_page = alloc_page(GFP_KERNEL); - if (!pte_page) - return -ENOMEM; + struct page *base = ptdesc_page(sd->ptdesc); + pte_t old = ptep_get(sd->kpte); + pgprot_t prot = __pgprot(pte_val(old) & ~_PAGE_PFN_MASK); + unsigned long pfn = pte_pfn(old); + unsigned long pfninc; + + switch (sd->level) { + case PGTABLE_LEVEL_PMD: + pfninc = 1; + break; + case PGTABLE_LEVEL_PUD: + pfninc = PMD_SIZE >> PAGE_SHIFT; + break; + case PGTABLE_LEVEL_P4D: + pfninc = PUD_SIZE >> PAGE_SHIFT; + break; + default: + return -EINVAL; + } - ptep_new = (pte_t *)page_address(pte_page); - for (i = 0; i < PTRS_PER_PTE; ++i, ++ptep_new) - set_pte(ptep_new, pfn_pte(pfn + i, prot)); + split_set_ptes((pte_t *)page_address(base), pfn, pfninc, prot); - smp_wmb(); + smp_wmb(); + set_pte(sd->kpte, pfn_pte(page_to_pfn(base), PAGE_TABLE)); - set_pmd(pmdp, pfn_pmd(page_to_pfn(pte_page), PAGE_TABLE)); - } - } while (pmdp++, vaddr = next, vaddr != end); + cpa->flags |= CPA_FLUSHTLB; + cpa->force_flush_all = 1; return 0; +#else + WARN_ON_ONCE(1); + return -EINVAL; +#endif } -static int __split_linear_mapping_pud(p4d_t *p4dp, - unsigned long vaddr, unsigned long end) +void arch_change_pte(struct cpa_data *cpa, unsigned long address, + pte_t *kpte, pte_t old_pte, bool nx, bool rw) { - pud_t *pudp; - unsigned long next; - int ret; - - pudp = pud_offset(p4dp, vaddr); - - do { - next = pud_addr_end(vaddr, end); - - if (next - vaddr >= PUD_SIZE && - vaddr <= (vaddr & PUD_MASK) && end >= next) - continue; - - if (pud_leaf(pudp_get(pudp))) { - struct page *pmd_page; - unsigned long pfn = _pud_pfn(pudp_get(pudp)); - pgprot_t prot = __pgprot(pud_val(pudp_get(pudp)) & ~_PAGE_PFN_MASK); - pmd_t *pmdp_new; - int i; + pgprot_t new_prot = __pgprot(pte_val(old_pte) & ~_PAGE_PFN_MASK); + unsigned long pfn = pte_pfn(old_pte); + pte_t new_pte; - pmd_page = alloc_page(GFP_KERNEL); - if (!pmd_page) - return -ENOMEM; + pgprot_val(new_prot) &= ~pgprot_val(cpa->mask_clr); + pgprot_val(new_prot) |= pgprot_val(cpa->mask_set); - pmdp_new = (pmd_t *)page_address(pmd_page); - for (i = 0; i < PTRS_PER_PMD; ++i, ++pmdp_new) - set_pmd(pmdp_new, - pfn_pmd(pfn + ((i * PMD_SIZE) >> PAGE_SHIFT), prot)); + new_pte = pfn_pte(pfn, new_prot); + cpa->pfn = pfn; - smp_wmb(); - - set_pud(pudp, pfn_pud(page_to_pfn(pmd_page), PAGE_TABLE)); - } - - ret = __split_linear_mapping_pmd(pudp, vaddr, next); - if (ret) - return ret; - } while (pudp++, vaddr = next, vaddr != end); - - return 0; + if (pte_val(old_pte) != pte_val(new_pte)) { + set_pte(kpte, new_pte); + cpa->flags |= CPA_FLUSHTLB; + } } -static int __split_linear_mapping_p4d(pgd_t *pgdp, - unsigned long vaddr, unsigned long end) +int arch_cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, + int primary) { - p4d_t *p4dp; - unsigned long next; - int ret; - - p4dp = p4d_offset(pgdp, vaddr); - - do { - next = p4d_addr_end(vaddr, end); - - /* - * If [vaddr; end] contains [vaddr & P4D_MASK; next], we don't - * need to split, we'll change the protections on the whole P4D. - */ - if (next - vaddr >= P4D_SIZE && - vaddr <= (vaddr & P4D_MASK) && end >= next) - continue; - - if (p4d_leaf(p4dp_get(p4dp))) { - struct page *pud_page; - unsigned long pfn = _p4d_pfn(p4dp_get(p4dp)); - pgprot_t prot = __pgprot(p4d_val(p4dp_get(p4dp)) & ~_PAGE_PFN_MASK); - pud_t *pudp_new; - int i; - - pud_page = alloc_page(GFP_KERNEL); - if (!pud_page) - return -ENOMEM; - - /* - * Fill the pud level with leaf puds that have the same - * protections as the leaf p4d. - */ - pudp_new = (pud_t *)page_address(pud_page); - for (i = 0; i < PTRS_PER_PUD; ++i, ++pudp_new) - set_pud(pudp_new, - pfn_pud(pfn + ((i * PUD_SIZE) >> PAGE_SHIFT), prot)); - - /* - * Make sure the pud filling is not reordered with the - * p4d store which could result in seeing a partially - * filled pud level. - */ - smp_wmb(); - - set_p4d(p4dp, pfn_p4d(page_to_pfn(pud_page), PAGE_TABLE)); - } - - ret = __split_linear_mapping_pud(p4dp, vaddr, next); - if (ret) - return ret; - } while (p4dp++, vaddr = next, vaddr != end); + cpa->numpages = 1; - return 0; -} - -static int __split_linear_mapping_pgd(pgd_t *pgdp, - unsigned long vaddr, - unsigned long end) -{ - unsigned long next; - int ret; + if (!primary) + return 0; - do { - next = pgd_addr_end(vaddr, end); - /* We never use PGD mappings for the linear mapping */ - ret = __split_linear_mapping_p4d(pgdp, vaddr, next); - if (ret) - return ret; - } while (pgdp++, vaddr = next, vaddr != end); + /* The linear map is expected to have holes */ + if (is_linear_mapping(vaddr)) { + cpa->pfn = PFN_DOWN(__pa(vaddr)); + return 0; + } - return 0; -} + WARN(1, "CPA: called for zero pte. vaddr = %lx cpa->vaddr = %lx\n", + vaddr, *cpa->vaddr); -static int split_linear_mapping(unsigned long start, unsigned long end) -{ - return __split_linear_mapping_pgd(pgd_offset_k(start), start, end); + return -EFAULT; } -#endif /* CONFIG_64BIT */ -static int __set_memory(unsigned long addr, int numpages, pgprot_t set_mask, - pgprot_t clear_mask) +int arch_cpa_process_alias(struct cpa_data *cpa) { - int ret; - unsigned long start = addr; - unsigned long end = start + PAGE_SIZE * numpages; - unsigned long __maybe_unused lm_start; - unsigned long __maybe_unused lm_end; - struct pageattr_masks masks = { - .set_mask = set_mask, - .clear_mask = clear_mask - }; - - if (!numpages) - return 0; - - mmap_write_lock(&init_mm); + struct cpa_data alias_cpa; + unsigned long laddr; -#ifdef CONFIG_64BIT /* - * We are about to change the permissions of a kernel mapping, we must - * apply the same changes to its linear mapping alias, which may imply - * splitting a huge mapping. + * cpa_should_update_alias() only lets non linear map primaries reach + * here, so @cpa->pfn always has a linear map alias that must receive + * the same protection change. */ + laddr = (unsigned long)__va(PFN_PHYS(cpa->pfn)); - if (is_vmalloc_or_module_addr((void *)start)) { - struct vm_struct *area = NULL; - int i, page_start; - - area = find_vm_area((void *)start); - page_start = (start - (unsigned long)area->addr) >> PAGE_SHIFT; - - for (i = page_start; i < page_start + numpages; ++i) { - lm_start = (unsigned long)page_address(area->pages[i]); - lm_end = lm_start + PAGE_SIZE; - - ret = split_linear_mapping(lm_start, lm_end); - if (ret) - goto unlock; - - ret = walk_kernel_page_table_range(lm_start, lm_end, - &pageattr_ops, NULL, &masks); - if (ret) - goto unlock; - } - } else if (is_kernel_mapping(start) || is_linear_mapping(start)) { - if (is_kernel_mapping(start)) { - lm_start = (unsigned long)lm_alias(start); - lm_end = (unsigned long)lm_alias(end); - } else { - lm_start = start; - lm_end = end; - } - - ret = split_linear_mapping(lm_start, lm_end); - if (ret) - goto unlock; - - ret = walk_kernel_page_table_range(lm_start, lm_end, - &pageattr_ops, NULL, &masks); - if (ret) - goto unlock; - } - - ret = walk_kernel_page_table_range(start, end, &pageattr_ops, NULL, - &masks); + alias_cpa = *cpa; + alias_cpa.vaddr = &laddr; + alias_cpa.flags &= ~(CPA_PAGES_ARRAY | CPA_ARRAY); + alias_cpa.curpage = 0; -unlock: - mmap_write_unlock(&init_mm); + /* The linear map alias must never be made executable */ + alias_cpa.mask_set = __pgprot(pgprot_val(alias_cpa.mask_set) & ~_PAGE_EXEC); + alias_cpa.mask_clr = __pgprot(pgprot_val(alias_cpa.mask_clr) & ~_PAGE_EXEC); - /* - * We can't use flush_tlb_kernel_range() here as we may have split a - * hugepage that is larger than that, so let's flush everything. - */ - flush_tlb_all(); -#else - ret = walk_kernel_page_table_range(start, end, &pageattr_ops, NULL, - &masks); + cpa->force_flush_all = 1; - mmap_write_unlock(&init_mm); + return __change_page_attr_set_clr(&alias_cpa, 0); +} - flush_tlb_kernel_range(start, end); -#endif +void arch_cpa_flush(struct cpa_data *cpa, int err) +{ + if (err || cpa->force_flush_all || + (cpa->flags & (CPA_ARRAY | CPA_PAGES_ARRAY))) { + flush_tlb_all(); + return; + } - return ret; + flush_tlb_kernel_range(*cpa->vaddr, + *cpa->vaddr + cpa->numpages * PAGE_SIZE); } int set_memory_rw_nx(unsigned long addr, int numpages) { - return __set_memory(addr, numpages, __pgprot(_PAGE_READ | _PAGE_WRITE), - __pgprot(_PAGE_EXEC)); + return change_page_attr_set_clr(&addr, numpages, + __pgprot(_PAGE_READ | _PAGE_WRITE), + __pgprot(_PAGE_EXEC), 0, 0, NULL); } int set_memory_ro(unsigned long addr, int numpages) { - return __set_memory(addr, numpages, __pgprot(_PAGE_READ), - __pgprot(_PAGE_WRITE)); + return change_page_attr_set_clr(&addr, numpages, __pgprot(_PAGE_READ), + __pgprot(_PAGE_WRITE), 0, 0, NULL); } int set_memory_rw(unsigned long addr, int numpages) { - return __set_memory(addr, numpages, __pgprot(_PAGE_READ | _PAGE_WRITE), - __pgprot(0)); + return change_page_attr_set(&addr, numpages, + __pgprot(_PAGE_READ | _PAGE_WRITE), 0); } int set_memory_x(unsigned long addr, int numpages) { - return __set_memory(addr, numpages, __pgprot(_PAGE_EXEC), __pgprot(0)); + return change_page_attr_set(&addr, numpages, __pgprot(_PAGE_EXEC), 0); } int set_memory_nx(unsigned long addr, int numpages) { - return __set_memory(addr, numpages, __pgprot(0), __pgprot(_PAGE_EXEC)); + return change_page_attr_clear(&addr, numpages, __pgprot(_PAGE_EXEC), 0); } int set_direct_map_invalid_noflush(struct page *page) { - return __set_memory((unsigned long)page_address(page), 1, - __pgprot(0), __pgprot(_PAGE_PRESENT)); + unsigned long start = (unsigned long)page_address(page); + + return change_page_attr_clear(&start, 1, __pgprot(_PAGE_PRESENT), 0); } int set_direct_map_default_noflush(struct page *page) { - return __set_memory((unsigned long)page_address(page), 1, - PAGE_KERNEL, __pgprot(_PAGE_EXEC)); + unsigned long start = (unsigned long)page_address(page); + + return change_page_attr_set_clr(&start, 1, PAGE_KERNEL, + __pgprot(_PAGE_EXEC), 0, 0, NULL); } int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) { + unsigned long start = (unsigned long)page_address(page); pgprot_t set, clear; if (valid) { @@ -398,7 +224,7 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) clear = __pgprot(_PAGE_PRESENT); } - return __set_memory((unsigned long)page_address(page), nr, set, clear); + return change_page_attr_set_clr(&start, nr, set, clear, 0, 0, NULL); } #ifdef CONFIG_DEBUG_PAGEALLOC @@ -435,36 +261,11 @@ void __kernel_map_pages(struct page *page, int numpages, int enable) bool kernel_page_present(struct page *page) { unsigned long addr = (unsigned long)page_address(page); - pgd_t *pgd; - pud_t *pud; - p4d_t *p4d; - pmd_t *pmd; - pte_t *pte; - - pgd = pgd_offset_k(addr); - if (!pgd_present(pgdp_get(pgd))) - return false; - if (pgd_leaf(pgdp_get(pgd))) - return true; - - p4d = p4d_offset(pgd, addr); - if (!p4d_present(p4dp_get(p4d))) - return false; - if (p4d_leaf(p4dp_get(p4d))) - return true; - - pud = pud_offset(p4d, addr); - if (!pud_present(pudp_get(pud))) - return false; - if (pud_leaf(pudp_get(pud))) - return true; + unsigned int level; + pte_t *pte = lookup_address(addr, &level); - pmd = pmd_offset(pud, addr); - if (!pmd_present(pmdp_get(pmd))) + if (!pte) return false; - if (pmd_leaf(pmdp_get(pmd))) - return true; - pte = pte_offset_kernel(pmd, addr); return pte_present(ptep_get(pte)); } diff --git a/include/linux/set_memory.h b/include/linux/set_memory.h index 86a7bef7cbca..17cbe6e26cb7 100644 --- a/include/linux/set_memory.h +++ b/include/linux/set_memory.h @@ -129,6 +129,12 @@ struct cpa_split_data { unsigned long cpa_addr(struct cpa_data *cpa, unsigned long idx); +pte_t *lookup_address(unsigned long address, unsigned int *level); +pte_t *lookup_address_in_pgd(pgd_t *pgd, unsigned long address, + unsigned int *level); +pte_t *lookup_address_in_pgd_attr(pgd_t *pgd, unsigned long address, + unsigned int *level, bool *nx, bool *rw); + int arch_cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, int primary); int arch_cpa_process_alias(struct cpa_data *cpa); -- 2.53.0 riscv versions of set_direct_memory_{default,invalid,valid}_noflush call core CPA API (change_page_attr_set_clr()) that actually flush TLB after the page table update. This was also the case before the conversion of riscv to use generic CPA core, riscv::__set_memory() always flushed TLBs, even when it was called from _noflush API variants. Switch the _noflush APIs to use __change_page_attr_set_clr() that does not flush TLB unless there was an error or there was a split of higher level page tables. Signed-off-by: Mike Rapoport (Microsoft) --- arch/riscv/mm/pageattr.c | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/arch/riscv/mm/pageattr.c b/arch/riscv/mm/pageattr.c index 677f42a6750d..66a20fc3d805 100644 --- a/arch/riscv/mm/pageattr.c +++ b/arch/riscv/mm/pageattr.c @@ -199,32 +199,46 @@ int set_memory_nx(unsigned long addr, int numpages) int set_direct_map_invalid_noflush(struct page *page) { unsigned long start = (unsigned long)page_address(page); - - return change_page_attr_clear(&start, 1, __pgprot(_PAGE_PRESENT), 0); + struct cpa_data cpa = { .vaddr = &start, + .pgd = NULL, + .numpages = 1, + .mask_set = __pgprot(0), + .mask_clr = __pgprot(_PAGE_PRESENT), + .flags = CPA_NO_CHECK_ALIAS }; + + return __change_page_attr_set_clr(&cpa, 1); } int set_direct_map_default_noflush(struct page *page) { unsigned long start = (unsigned long)page_address(page); - - return change_page_attr_set_clr(&start, 1, PAGE_KERNEL, - __pgprot(_PAGE_EXEC), 0, 0, NULL); + struct cpa_data cpa = { .vaddr = &start, + .pgd = NULL, + .numpages = 1, + .mask_set = PAGE_KERNEL, + .mask_clr = __pgprot(_PAGE_EXEC), + .flags = CPA_NO_CHECK_ALIAS }; + + return __change_page_attr_set_clr(&cpa, 1); } int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) { unsigned long start = (unsigned long)page_address(page); - pgprot_t set, clear; + struct cpa_data cpa = { .vaddr = &start, + .pgd = NULL, + .numpages = nr, + .flags = CPA_NO_CHECK_ALIAS }; if (valid) { - set = PAGE_KERNEL; - clear = __pgprot(_PAGE_EXEC); + cpa.mask_set = PAGE_KERNEL; + cpa.mask_clr = __pgprot(_PAGE_EXEC); } else { - set = __pgprot(0); - clear = __pgprot(_PAGE_PRESENT); + cpa.mask_set = __pgprot(0); + cpa.mask_clr = __pgprot(_PAGE_PRESENT); } - return change_page_attr_set_clr(&start, nr, set, clear, 0, 0, NULL); + return __change_page_attr_set_clr(&cpa, 1); } #ifdef CONFIG_DEBUG_PAGEALLOC -- 2.53.0 Replace the pageattr_ops based page table walk in arch/loongarch/mm/pageattr.c with the common change_page_attr() loop, implement the required architecture hooks and opt-in for CONFIG_GENERIC_SET_MEMORY. Instead if pre-splitting large pages with split_linear_mapping() use loongarch-specific implementation of arch_should_split_large_page() and arch_split_large_page(). Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/loongarch/Kconfig | 1 + arch/loongarch/include/asm/pgtable.h | 42 ++++++++ arch/loongarch/include/asm/set_memory.h | 9 ++ arch/loongarch/mm/pageattr.c | 173 ++++++++++---------------------- 4 files changed, 106 insertions(+), 119 deletions(-) diff --git a/arch/loongarch/Kconfig b/arch/loongarch/Kconfig index d8d252325017..1bfa94797b1b 100644 --- a/arch/loongarch/Kconfig +++ b/arch/loongarch/Kconfig @@ -109,6 +109,7 @@ config LOONGARCH select GENERIC_LIB_DEVMEM_IS_ALLOWED select GENERIC_PCI_IOMAP select GENERIC_SCHED_CLOCK + select GENERIC_SET_MEMORY select GENERIC_SMP_IDLE_THREAD select GPIOLIB select HAS_IOPORT diff --git a/arch/loongarch/include/asm/pgtable.h b/arch/loongarch/include/asm/pgtable.h index 223528c04d73..377bef0894d7 100644 --- a/arch/loongarch/include/asm/pgtable.h +++ b/arch/loongarch/include/asm/pgtable.h @@ -638,6 +638,48 @@ static inline long pmd_protnone(pmd_t pmd) #define pmd_leaf(pmd) ((pmd_val(pmd) & _PAGE_HUGE) != 0) #define pud_leaf(pud) ((pud_val(pud) & _PAGE_HUGE) != 0) +#define pgd_write pgd_write +static inline int pgd_write(pgd_t pgd) +{ + return !!(pgd_val(pgd) & _PAGE_WRITE); +} + +#define p4d_write p4d_write +static inline int p4d_write(p4d_t p4d) +{ + return !!(p4d_val(p4d) & _PAGE_WRITE); +} + +#define pud_write pud_write +static inline int pud_write(pud_t pud) +{ + return !!(pud_val(pud) & _PAGE_WRITE); +} + +#define pgd_exec pgd_exec +static inline int pgd_exec(pgd_t pgd) +{ + return !(pgd_val(pgd) & _PAGE_NO_EXEC); +} + +#define p4d_exec p4d_exec +static inline int p4d_exec(p4d_t p4d) +{ + return !(p4d_val(p4d) & _PAGE_NO_EXEC); +} + +#define pud_exec pud_exec +static inline int pud_exec(pud_t pud) +{ + return !(pud_val(pud) & _PAGE_NO_EXEC); +} + +#define pmd_exec pmd_exec +static inline int pmd_exec(pmd_t pmd) +{ + return !(pmd_val(pmd) & _PAGE_NO_EXEC); +} + /* * We provide our own get_unmapped area to cope with the virtual aliasing * constraints placed on us by the cache architecture. diff --git a/arch/loongarch/include/asm/set_memory.h b/arch/loongarch/include/asm/set_memory.h index 55dfaefd02c8..6ffb4fe5a75b 100644 --- a/arch/loongarch/include/asm/set_memory.h +++ b/arch/loongarch/include/asm/set_memory.h @@ -19,4 +19,13 @@ int set_direct_map_default_noflush(struct page *page); int set_direct_map_invalid_noflush(struct page *page); int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid); +/* + * The direct map is a fixed address window that is not backed by page tables, + * so a page never has a second mapping whose protection must be synchronised. + */ +static inline bool cpa_should_update_alias(unsigned long vaddr, unsigned long pfn) +{ + return false; +} + #endif /* _ASM_LOONGARCH_SET_MEMORY_H */ diff --git a/arch/loongarch/mm/pageattr.c b/arch/loongarch/mm/pageattr.c index 614ccc7afccb..4500769ccf6a 100644 --- a/arch/loongarch/mm/pageattr.c +++ b/arch/loongarch/mm/pageattr.c @@ -4,126 +4,83 @@ */ #include -#include +#include #include +#include #include #include -struct pageattr_masks { - pgprot_t set_mask; - pgprot_t clear_mask; -}; - -static unsigned long set_pageattr_masks(unsigned long val, struct mm_walk *walk) +static unsigned long set_pageattr_masks(struct cpa_data *cpa, unsigned long val) { - unsigned long new_val = val; - struct pageattr_masks *masks = walk->private; - - new_val &= ~(pgprot_val(masks->clear_mask)); - new_val |= (pgprot_val(masks->set_mask)); + val &= ~pgprot_val(cpa->mask_clr); + val |= pgprot_val(cpa->mask_set); - return new_val; + return val; } -static int pageattr_pgd_entry(pgd_t *pgd, unsigned long addr, - unsigned long next, struct mm_walk *walk) +int arch_should_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - pgd_t val = pgdp_get(pgd); - - if (pgd_leaf(val)) { - val = __pgd(set_pageattr_masks(pgd_val(val), walk)); - set_pgd(pgd, val); - } - - return 0; -} + pte_t old = ptep_get(sd->kpte); + unsigned long val = pte_val(old); + unsigned long new_val = set_pageattr_masks(cpa, val); -static int pageattr_p4d_entry(p4d_t *p4d, unsigned long addr, - unsigned long next, struct mm_walk *walk) -{ - p4d_t val = p4dp_get(p4d); + cpa->pfn = pte_pfn(old) + ((sd->address & (sd->psize - 1)) >> PAGE_SHIFT); - if (p4d_leaf(val)) { - val = __p4d(set_pageattr_masks(p4d_val(val), walk)); - set_p4d(p4d, val); + if (new_val != val) { + set_pte(sd->kpte, __pte(new_val)); + cpa->flags |= CPA_FLUSHTLB; } return 0; } -static int pageattr_pud_entry(pud_t *pud, unsigned long addr, - unsigned long next, struct mm_walk *walk) +int arch_split_large_page(struct cpa_data *cpa, struct cpa_split_data *sd) { - pud_t val = pudp_get(pud); - - if (pud_leaf(val)) { - val = __pud(set_pageattr_masks(pud_val(val), walk)); - set_pud(pud, val); - } + WARN_ON_ONCE(1); - return 0; + return -EINVAL; } -static int pageattr_pmd_entry(pmd_t *pmd, unsigned long addr, - unsigned long next, struct mm_walk *walk) +void arch_change_pte(struct cpa_data *cpa, unsigned long address, + pte_t *kpte, pte_t old_pte, bool nx, bool rw) { - pmd_t val = pmdp_get(pmd); + unsigned long val = pte_val(old_pte); + pte_t new_pte = __pte(set_pageattr_masks(cpa, val)); - if (pmd_leaf(val)) { - val = __pmd(set_pageattr_masks(pmd_val(val), walk)); - set_pmd(pmd, val); - } + cpa->pfn = pte_pfn(old_pte); - return 0; + if (pte_val(old_pte) != pte_val(new_pte)) { + set_pte(kpte, new_pte); + cpa->flags |= CPA_FLUSHTLB; + } } -static int pageattr_pte_entry(pte_t *pte, unsigned long addr, - unsigned long next, struct mm_walk *walk) +int arch_cpa_process_fault(struct cpa_data *cpa, unsigned long vaddr, + int primary) { - pte_t val = ptep_get(pte); - - val = __pte(set_pageattr_masks(pte_val(val), walk)); - set_pte(pte, val); + /* + * The direct map has no page tables and vmalloc ranges may contain + * holes. Both are silently skipped. + */ + cpa->numpages = 1; return 0; } -static int pageattr_pte_hole(unsigned long addr, unsigned long next, - int depth, struct mm_walk *walk) +int arch_cpa_process_alias(struct cpa_data *cpa) { return 0; } -static const struct mm_walk_ops pageattr_ops = { - .pgd_entry = pageattr_pgd_entry, - .p4d_entry = pageattr_p4d_entry, - .pud_entry = pageattr_pud_entry, - .pmd_entry = pageattr_pmd_entry, - .pte_entry = pageattr_pte_entry, - .pte_hole = pageattr_pte_hole, - .walk_lock = PGWALK_RDLOCK, -}; - -static int __set_memory(unsigned long addr, int numpages, pgprot_t set_mask, pgprot_t clear_mask) +void arch_cpa_flush(struct cpa_data *cpa, int err) { - int ret; - unsigned long start = addr; - unsigned long end = start + PAGE_SIZE * numpages; - struct pageattr_masks masks = { - .set_mask = set_mask, - .clear_mask = clear_mask - }; - - if (!numpages) - return 0; - - mmap_write_lock(&init_mm); - ret = walk_kernel_page_table_range(start, end, &pageattr_ops, NULL, &masks); - mmap_write_unlock(&init_mm); - - flush_tlb_kernel_range(start, end); + if (err || cpa->force_flush_all || + (cpa->flags & (CPA_ARRAY | CPA_PAGES_ARRAY))) { + flush_tlb_all(); + return; + } - return ret; + flush_tlb_kernel_range(*cpa->vaddr, *cpa->vaddr + cpa->numpages * PAGE_SIZE); } int set_memory_x(unsigned long addr, int numpages) @@ -131,7 +88,7 @@ int set_memory_x(unsigned long addr, int numpages) if (addr < vm_map_base) return 0; - return __set_memory(addr, numpages, __pgprot(0), __pgprot(_PAGE_NO_EXEC)); + return change_page_attr_clear(&addr, numpages, __pgprot(_PAGE_NO_EXEC), 0); } int set_memory_nx(unsigned long addr, int numpages) @@ -139,7 +96,7 @@ int set_memory_nx(unsigned long addr, int numpages) if (addr < vm_map_base) return 0; - return __set_memory(addr, numpages, __pgprot(_PAGE_NO_EXEC), __pgprot(0)); + return change_page_attr_set(&addr, numpages, __pgprot(_PAGE_NO_EXEC), 0); } int set_memory_ro(unsigned long addr, int numpages) @@ -147,7 +104,7 @@ int set_memory_ro(unsigned long addr, int numpages) if (addr < vm_map_base) return 0; - return __set_memory(addr, numpages, __pgprot(0), __pgprot(_PAGE_WRITE | _PAGE_DIRTY)); + return change_page_attr_clear(&addr, numpages, __pgprot(_PAGE_WRITE | _PAGE_DIRTY), 0); } int set_memory_rw(unsigned long addr, int numpages) @@ -155,46 +112,22 @@ int set_memory_rw(unsigned long addr, int numpages) if (addr < vm_map_base) return 0; - return __set_memory(addr, numpages, __pgprot(_PAGE_WRITE | _PAGE_DIRTY), __pgprot(0)); + return change_page_attr_set(&addr, numpages, __pgprot(_PAGE_WRITE | _PAGE_DIRTY), 0); } bool kernel_page_present(struct page *page) { - pgd_t *pgd; - p4d_t *p4d; - pud_t *pud; - pmd_t *pmd; - pte_t *pte; + unsigned int level; unsigned long addr = (unsigned long)page_address(page); + pte_t *pte; if (addr < vm_map_base) return memblock_is_memory(__pa(addr)); - pgd = pgd_offset_k(addr); - if (pgd_none(pgdp_get(pgd))) - return false; - if (pgd_leaf(pgdp_get(pgd))) - return true; - - p4d = p4d_offset(pgd, addr); - if (p4d_none(p4dp_get(p4d))) - return false; - if (p4d_leaf(p4dp_get(p4d))) - return true; - - pud = pud_offset(p4d, addr); - if (pud_none(pudp_get(pud))) - return false; - if (pud_leaf(pudp_get(pud))) - return true; - - pmd = pmd_offset(pud, addr); - if (pmd_none(pmdp_get(pmd))) + pte = lookup_address(addr, &level); + if (!pte) return false; - if (pmd_leaf(pmdp_get(pmd))) - return true; - pte = pte_offset_kernel(pmd, addr); return pte_present(ptep_get(pte)); } @@ -205,7 +138,8 @@ int set_direct_map_default_noflush(struct page *page) if (addr < vm_map_base) return 0; - return __set_memory(addr, 1, PAGE_KERNEL, __pgprot(0)); + return change_page_attr_set_clr(&addr, 1, PAGE_KERNEL, + __pgprot(0), 0, 0, NULL); } int set_direct_map_invalid_noflush(struct page *page) @@ -215,7 +149,8 @@ int set_direct_map_invalid_noflush(struct page *page) if (addr < vm_map_base) return 0; - return __set_memory(addr, 1, __pgprot(0), __pgprot(_PAGE_PRESENT | _PAGE_VALID)); + return change_page_attr_clear(&addr, 1, + __pgprot(_PAGE_PRESENT | _PAGE_VALID), 0); } int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) @@ -234,5 +169,5 @@ int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) clear = __pgprot(_PAGE_PRESENT | _PAGE_VALID); } - return __set_memory(addr, nr, set, clear); + return change_page_attr_set_clr(&addr, nr, set, clear, 0, 0, NULL); } -- 2.53.0 loongarch versions of set_direct_memory_{default,invalid,valid}_noflush call core CPA API (change_page_attr_set_clr()) that actually flush TLB after the page table update. This was also the case before the conversion of loongarch to use generic CPA core, loongarch::__set_memory() always flushed TLBs, even when it was called from _noflush API variants. Switch the _noflush APIs to use __change_page_attr_set_clr() that does not flush TLB unless there was an error or there was a split of higher level page tables. Assisted-by: Copilot:claude-opus-4.8 Signed-off-by: Mike Rapoport (Microsoft) --- arch/loongarch/mm/pageattr.c | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/arch/loongarch/mm/pageattr.c b/arch/loongarch/mm/pageattr.c index 4500769ccf6a..dbbd408bd3d5 100644 --- a/arch/loongarch/mm/pageattr.c +++ b/arch/loongarch/mm/pageattr.c @@ -134,40 +134,53 @@ bool kernel_page_present(struct page *page) int set_direct_map_default_noflush(struct page *page) { unsigned long addr = (unsigned long)page_address(page); + struct cpa_data cpa = { .vaddr = &addr, + .pgd = NULL, + .numpages = 1, + .mask_set = PAGE_KERNEL, + .mask_clr = __pgprot(0), + .flags = CPA_NO_CHECK_ALIAS }; if (addr < vm_map_base) return 0; - return change_page_attr_set_clr(&addr, 1, PAGE_KERNEL, - __pgprot(0), 0, 0, NULL); + return __change_page_attr_set_clr(&cpa, 1); } int set_direct_map_invalid_noflush(struct page *page) { unsigned long addr = (unsigned long)page_address(page); + struct cpa_data cpa = { .vaddr = &addr, + .pgd = NULL, + .numpages = 1, + .mask_set = __pgprot(0), + .mask_clr = __pgprot(_PAGE_PRESENT | _PAGE_VALID), + .flags = CPA_NO_CHECK_ALIAS }; if (addr < vm_map_base) return 0; - return change_page_attr_clear(&addr, 1, - __pgprot(_PAGE_PRESENT | _PAGE_VALID), 0); + return __change_page_attr_set_clr(&cpa, 1); } int set_direct_map_valid_noflush(struct page *page, unsigned nr, bool valid) { unsigned long addr = (unsigned long)page_address(page); - pgprot_t set, clear; + struct cpa_data cpa = { .vaddr = &addr, + .pgd = NULL, + .numpages = nr, + .flags = CPA_NO_CHECK_ALIAS }; if (addr < vm_map_base) return 0; if (valid) { - set = PAGE_KERNEL; - clear = __pgprot(0); + cpa.mask_set = PAGE_KERNEL; + cpa.mask_clr = __pgprot(0); } else { - set = __pgprot(0); - clear = __pgprot(_PAGE_PRESENT | _PAGE_VALID); + cpa.mask_set = __pgprot(0); + cpa.mask_clr = __pgprot(_PAGE_PRESENT | _PAGE_VALID); } - return change_page_attr_set_clr(&addr, nr, set, clear, 0, 0, NULL); + return __change_page_attr_set_clr(&cpa, 1); } -- 2.53.0