From 486d396b19e93f6e56e0a0ba96c14a9ccc1a5977 Mon Sep 17 00:00:00 2001 From: Zhao Gongyi Date: Fri, 23 Oct 2026 20:12:33 +0800 Subject: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs The transparent contpte layer treats PTE_CONT as software grouping state. When a valid PTE has PTE_CONT set, the contpte helpers may align an operation to a CONT_PTE_SIZE block and operate on the whole block. The current single-PTE helpers use pte_valid_cont() to identify such groups; this is a helper contract, not an architectural restriction on using PTE_CONT with present-invalid descriptors. Explicit users such as hugetlb manage their contiguous encodings separately. contpte_set_ptes() currently sets PTE_CONT from alignment alone. Batched set_ptes() users can therefore install present-invalid PTEs with PTE_CONT set, for example mprotect(PROT_NONE), NUMA hinting through modify_prot_commit_ptes(), fork() copying a PROT_NONE large folio, or a PMD split installing PAGE_NONE PTEs. This is not a hardware programming problem: invalid descriptors are not translated, so the hardware ignores descriptor bits such as PTE_CONT. It is a software-state problem. Some arm64 fast paths, such as test_and_clear_young_ptes(), test pte_cont() directly instead of pte_valid_cont(). In the observed failure, mprotect(PROT_NONE) installed a full CONT_PTE_SIZE range of present-invalid PTEs with PTE_CONT set. MADV_GUARD_INSTALL then zapped one PTE. The nr == 1 clear_full_ptes() path only unfolds pte_valid_cont() entries, so the neighbouring present-invalid entries kept PTE_CONT while the target entry became a guard marker. clear_refs_pte_range() then reached the marker through: ptep_test_and_clear_young() test_and_clear_young_ptes() contpte_test_and_clear_young_ptes() contpte_align_addr_ptep() __ptep_test_and_clear_young() The marker changed from old_pte=0x00000000000047c0 to new_pte=0x00000000000043c0 by clearing PTE_AF. For swap PTEs, bit 10 is part of the swap type field, bits 6-10: 0x47c0 is SWP_PTE_MARKER with offset 4, while 0x43c0 has type 15 and is decoded as an ordinary swap entry. Zap later warned in swap_put_entries_direct() with a bad swap entry. Present-invalid PTEs could retain PTE_CONT as software grouping state, but their single-PTE modification paths would then need to recognize and unfold those groups consistently. Since current helpers mostly use pte_valid_cont() for membership, avoid creating present-invalid transparent contpte groups in contpte_set_ptes(). They bring no hardware benefit while invalid, and eligible valid ranges can be grouped again when they are installed. The public set_ptes() wrapper already clears PTE_CONT before calling contpte_set_ptes(). Return invalid PTEs through __set_ptes() directly, and warn if a future direct caller unexpectedly passes an invalid PTE with PTE_CONT set. This is triggerable by a user process against its own address space. On systems running with panic_on_warn, the warning becomes a denial of service. Fixes: 4602e5757bcc ("arm64/mm: wire up PTE_CONT for user mappings") Cc: stable@vger.kernel.org Reviewed-by: Baolin Wang Tested-by: Lance Yang Signed-off-by: Zhao Gongyi Signed-off-by: Catalin Marinas --- arch/arm64/include/asm/pgtable.h | 3 +++ arch/arm64/mm/contpte.c | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h index 6000905a2e86..6c81a3d22d94 100644 --- a/arch/arm64/include/asm/pgtable.h +++ b/arch/arm64/include/asm/pgtable.h @@ -1651,6 +1651,9 @@ extern void modify_prot_commit_ptes(struct vm_area_struct *vma, unsigned long ad * The contpte APIs are used to transparently manage the contiguous bit in ptes * where it is possible and makes sense to do so. The PTE_CONT bit is considered * a private implementation detail of the public ptep API (see below). + * Single-PTE transparent contpte helpers use pte_valid_cont() to identify + * grouped PTEs; explicit contiguous-encoding users such as hugetlb manage + * their entries separately. */ extern void __contpte_try_fold(struct mm_struct *mm, unsigned long addr, pte_t *ptep, pte_t pte); diff --git a/arch/arm64/mm/contpte.c b/arch/arm64/mm/contpte.c index 0acab179fc1a..be78d8602e51 100644 --- a/arch/arm64/mm/contpte.c +++ b/arch/arm64/mm/contpte.c @@ -460,8 +460,10 @@ void contpte_set_ptes(struct mm_struct *mm, unsigned long addr, */ VM_WARN_ON(nr == 1); - if (!mm_is_user(mm)) + if (!mm_is_user(mm) || !pte_valid(pte)) { + VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte)); return __set_ptes(mm, addr, ptep, pte, nr); + } end = addr + (nr << PAGE_SHIFT); pfn = pte_pfn(pte);