[PATCH v2] arm64/mm: restrict transparent contpte mappings to valid PTEs
From: Zhao Gongyi
Date: Fri Oct 23 2026 - 08:13:17 EST
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@xxxxxxxxxxxxxxx
Reviewed-by: Baolin Wang <baolin.wang@xxxxxxxxxxxxxxxxx>
Tested-by: Lance Yang <lance.yang@xxxxxxxxx>
Signed-off-by: Zhao Gongyi <zhaogongyi@xxxxxxxxxxxxx>
---
Changes in v2:
- Return invalid descriptors through __set_ptes() directly instead of
silently clearing PTE_CONT, as suggested by Lance Yang.
- Add VM_WARN_ON_ONCE() for an unexpected direct caller that passes an
invalid descriptor with PTE_CONT set.
- Describe valid PTE grouping as the current helper contract rather
than an architectural restriction, following David Hildenbrand's
feedback.
Validation on arm64 QEMU/KVM with 4 KiB pages, CONFIG_ARM64_CONTPTE=y,
CONFIG_TRANSPARENT_HUGEPAGE=y, CONFIG_KASAN=y and panic_on_warn=1:
Unfixed kernel: guard-marker reproducer warned and panicked on its
first iteration.
v2 code: 100 guard-marker iterations, five fork probe runs and
five PMD-split probe runs without warnings.
The runtime build used base af32da41b032; arch/arm64/mm/contpte.c
matched the v2 commit's Git blob ID. The pgtable.h change since that
run is comment-only. The v2 commit itself is based on 24a19b953e2a
and was not boot-tested separately.
---
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 6000905a2..6c81a3d22 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 0acab179f..be78d8602 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);
--
2.39.5 (Apple Git-154)