* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 9:36 [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs Zhao Gongyi
@ 2026-10-13 10:13 ` Dev Jain
2026-10-14 6:16 ` Zhao Gongyi
2026-10-13 15:39 ` Lance Yang
` (2 subsequent siblings)
3 siblings, 1 reply; 17+ messages in thread
From: Dev Jain @ 2026-10-13 10:13 UTC (permalink / raw)
To: Zhao Gongyi, catalin.marinas, will
Cc: ryan.roberts, baolin.wang, lance.yang, mark.rutland, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, linux-arm-kernel,
linux-mm, linux-kernel, stable
On 13/10/26 3:06 pm, Zhao Gongyi wrote:
> The transparent contpte layer treats PTE_CONT as software state: when a
> PTE has PTE_CONT set, the contpte helpers may align the operation to a
> CONT_PTE_SIZE block and operate on the whole block. That invariant only
> makes sense for valid mappings. 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 intentionally test
> pte_cont() directly instead of pte_valid_cont(), for example commit
> 9970a9a27ffc ("arm64: mm: implement the architecture-specific
> test_and_clear_young_ptes()"). Fixing the producer side preserves these
> fast paths and covers all consumers.
>
> 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.
>
> 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.
>
> Fix the root cause by making contpte_set_ptes() use the transparent
> contpte encoding only for valid descriptors. After this change, the
> transparent contpte layer preserves the invariant that pte_cont(pte)
> implies pte_valid(pte), so fast consumers can keep using PTE_CONT as
> sufficient evidence of a transparent contpte block.
>
> Fixes: 4602e5757bcc ("arm64/mm: wire up PTE_CONT for user mappings")
> Cc: stable@vger.kernel.org
> Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
> ---
> Validation on an arm64 QEMU/KVM KASAN setup with CONFIG_ARM64_4K_PAGES=y
> and CONFIG_ARM64_CONTPTE=y, using an out-of-tree guard-region reproducer:
>
> populate a CONT_PTE_SIZE anonymous THP range
> mprotect(PROT_NONE) on the range
> MADV_GUARD_INSTALL on one PTE in that range
> write "1" to /proc/self/clear_refs
> restore access and remove the guard marker
Thanks, could you please give the code of the reproducer. That will
save us some time.
>
> Unfixed kernel: failed in the first iteration, with 65 bad swap
> messages and a WARNING in swap_put_entries_direct()
> This patch: completed 100 iterations without warnings
>
> Additional producer-side instrumentation confirmed the same invalid
> batch state outside the mprotect reproducer:
>
> Unfixed fork(): copy_page_range() wrote out_cont=1
> Unfixed PMD split: split_huge_pmd_locked() wrote out_cont=1
> This patch: both paths wrote out_cont=0
>
> arch/arm64/include/asm/pgtable.h | 5 ++++-
> arch/arm64/mm/contpte.c | 7 +++++++
> 2 files changed, 11 insertions(+), 1 deletion(-)
>
> diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
> index 6000905a2..d6246042b 100644
> --- a/arch/arm64/include/asm/pgtable.h
> +++ b/arch/arm64/include/asm/pgtable.h
> @@ -1650,7 +1650,10 @@ 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).
> + * a private implementation detail of the public ptep API (see below). The
> + * transparent contpte layer maintains the invariant that pte_cont(pte) implies
> + * pte_valid(pte); 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..851fa2e44 100644
> --- a/arch/arm64/mm/contpte.c
> +++ b/arch/arm64/mm/contpte.c
> @@ -463,6 +463,13 @@ void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
> if (!mm_is_user(mm))
> return __set_ptes(mm, addr, ptep, pte, nr);
>
> + /*
> + * Keep the producer-side invariant documented in pgtable.h true even
> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
> + */
> + if (!pte_valid(pte))
> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
> +
> end = addr + (nr << PAGE_SHIFT);
> pfn = pte_pfn(pte);
> prot = pte_pgprot(pte);
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 10:13 ` Dev Jain
@ 2026-10-14 6:16 ` Zhao Gongyi
0 siblings, 0 replies; 17+ messages in thread
From: Zhao Gongyi @ 2026-10-14 6:16 UTC (permalink / raw)
To: Dev Jain, catalin.marinas, will
Cc: ryan.roberts, baolin.wang, lance.yang, mark.rutland, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, linux-arm-kernel,
linux-mm, linux-kernel, stable
On 2026/10/13 18:13, Dev Jain wrote:
>
>
> Thanks, could you please give the code of the reproducer. That will
> save us some time.
>
>
Hi Dev,
Sure, sorry for not including it in the first mail.
The standalone reproducer I used is below. It assumes an arm64 kernel
with CONFIG_ARM64_CONTPTE=y and a CONT_PTE_SIZE anonymous mTHP order
available. When run as root, it tries to enable THP and the matching
mTHP order through sysfs. These sysfs writes are best-effort; if the
matching order is not available or cannot be enabled, the reproducer
may not hit the issue.
On a 4K-page kernel, CONT_PTE_SIZE is 64KB.
Compile and run:
gcc -O2 -Wall contpte_guard_repro.c -o contpte_guard_repro
./contpte_guard_repro 100
On my unfixed KASAN kernel with panic_on_warn=1, the first iteration hit
the bad swap entry / swap_put_entries_direct() warning. With this patch,
100 iterations completed without warnings.
Here is the reproducer:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef MADV_HUGEPAGE
#define MADV_HUGEPAGE 14
#endif
#ifndef MADV_POPULATE_WRITE
#define MADV_POPULATE_WRITE 23
#endif
#ifndef MADV_GUARD_INSTALL
#define MADV_GUARD_INSTALL 102
#endif
#ifndef MADV_GUARD_REMOVE
#define MADV_GUARD_REMOVE 103
#endif
static void write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return;
(void)write(fd, val, strlen(val));
close(fd);
}
static void write_clear_refs(void)
{
int fd = open("/proc/self/clear_refs", O_WRONLY | O_CLOEXEC);
if (fd < 0) {
perror("open clear_refs");
exit(1);
}
if (write(fd, "1", 1) != 1) {
perror("write clear_refs");
exit(1);
}
close(fd);
}
static unsigned long contpte_size(unsigned long page_size)
{
if (page_size == 4096)
return 64 * 1024;
if (page_size == 16384 || page_size == 65536)
return 2 * 1024 * 1024;
fprintf(stderr, "unsupported page size %lu\n", page_size);
exit(1);
}
static void enable_mthp(unsigned long size)
{
char path[256];
write_file("/sys/kernel/mm/transparent_hugepage/enabled",
"always\n");
write_file("/sys/kernel/mm/transparent_hugepage/defrag", "always\n");
snprintf(path, sizeof(path),
"/sys/kernel/mm/transparent_hugepage/hugepages-%lukB/enabled",
size / 1024);
write_file(path, "always\n");
}
static void touch_range(char *p, unsigned long len, unsigned long
page_size)
{
for (unsigned long off = 0; off < len; off += page_size)
p[off] = (char)off;
}
int main(int argc, char **argv)
{
const int loops = argc > 1 ? atoi(argv[1]) : 1;
const unsigned long page_size = sysconf(_SC_PAGESIZE);
const unsigned long csize = contpte_size(page_size);
const unsigned long nr_pages = 2 * csize / page_size;
const unsigned long len = nr_pages * page_size;
const unsigned long map_len = len + csize;
const unsigned long guard_idx = csize / page_size - 3;
enable_mthp(csize);
for (int i = 0; i < loops; i++) {
char *map, *p;
map = mmap(NULL, map_len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (map == MAP_FAILED) {
perror("mmap");
return 1;
}
p = (char *)(((unsigned long)map + csize - 1) & ~(csize - 1));
if (madvise(p, len, MADV_HUGEPAGE))
perror("madvise MADV_HUGEPAGE");
if (madvise(p, len, MADV_POPULATE_WRITE))
perror("madvise MADV_POPULATE_WRITE");
touch_range(p, len, page_size);
if (mprotect(p, len, PROT_NONE)) {
perror("mprotect PROT_NONE");
return 1;
}
if (madvise(p + guard_idx * page_size, page_size,
MADV_GUARD_INSTALL)) {
perror("madvise MADV_GUARD_INSTALL");
return 1;
}
write_clear_refs();
if (munmap(map, map_len)) {
perror("munmap");
return 1;
}
}
return 0;
}
Thanks,
Zhao
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 9:36 [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs Zhao Gongyi
2026-10-13 10:13 ` Dev Jain
@ 2026-10-13 15:39 ` Lance Yang
2026-10-14 6:31 ` Zhao Gongyi
2026-10-14 9:41 ` David Hildenbrand (Arm)
2026-10-23 12:12 ` [PATCH v2] " Zhao Gongyi
3 siblings, 1 reply; 17+ messages in thread
From: Lance Yang @ 2026-10-13 15:39 UTC (permalink / raw)
To: zhaogongyi
Cc: catalin.marinas, will, ryan.roberts, dev.jain, baolin.wang,
lance.yang, mark.rutland, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, linux-arm-kernel, linux-mm, linux-kernel, stable
On Tue, Oct 13, 2026 at 05:36:00PM +0800, Zhao Gongyi wrote:
[...]
>
>Fixes: 4602e5757bcc ("arm64/mm: wire up PTE_CONT for user mappings")
>Cc: stable@vger.kernel.org
>Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
>---
Good catch! Thanks!
[...]
>diff --git a/arch/arm64/mm/contpte.c b/arch/arm64/mm/contpte.c
>index 0acab179f..851fa2e44 100644
>--- a/arch/arm64/mm/contpte.c
>+++ b/arch/arm64/mm/contpte.c
>@@ -463,6 +463,13 @@ void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
> if (!mm_is_user(mm))
> return __set_ptes(mm, addr, ptep, pte, nr);
>
>+ /*
>+ * Keep the producer-side invariant documented in pgtable.h true even
>+ * if a direct caller passes an invalid descriptor with PTE_CONT set.
>+ */
>+ if (!pte_valid(pte))
>+ return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
set_ptes() is the only in-tree caller, and it already clears PTE_CONT
before we get here ... no? Why not fold !pte_valid(pte) into the
!mm_is_user(mm) check below and return __set_ptes() directly?
if (!mm_is_user(mm) || !pte_valid(pte))
return __set_ptes(mm, addr, ptep, pte, nr);
If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
hide a caller bug :)
Cheers, Lance
>+
> end = addr + (nr << PAGE_SHIFT);
> pfn = pte_pfn(pte);
> prot = pte_pgprot(pte);
>--
>2.39.5 (Apple Git-154)
>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 15:39 ` Lance Yang
@ 2026-10-14 6:31 ` Zhao Gongyi
2026-10-14 6:46 ` Lance Yang
2026-10-14 10:03 ` Dev Jain
0 siblings, 2 replies; 17+ messages in thread
From: Zhao Gongyi @ 2026-10-14 6:31 UTC (permalink / raw)
To: Lance Yang
Cc: catalin.marinas, will, ryan.roberts, dev.jain, baolin.wang,
mark.rutland, akpm, david, ljs, liam, vbabka, rppt, surenb,
mhocko, linux-arm-kernel, linux-mm, linux-kernel, stable
On 10/13/26 11:39 PM, Lance Yang wrote:
>>
>> + /*
>> + * Keep the producer-side invariant documented in pgtable.h true even
>> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
>> + */
>> + if (!pte_valid(pte))
>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>
> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
> before we get here ... no? Why not fold !pte_valid(pte) into the
> !mm_is_user(mm) check below and return __set_ptes() directly?
>
> if (!mm_is_user(mm) || !pte_valid(pte))
> return __set_ptes(mm, addr, ptep, pte, nr);
>
> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
> hide a caller bug :)
>
Hi Lance,
Thanks for taking a look.
Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
path is unnecessary for the current in-tree path.
The issue I was trying to avoid is not that the incoming pte already has
PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
and then adds PTE_CONT again based on alignment. For an invalid descriptor,
that transparent contiguous encoding should be skipped entirely.
So your suggested shape makes sense to me. I will change this to return
through __set_ptes() directly for invalid descriptors, and add a warning
for any unexpected direct caller that passes an invalid PTE with PTE_CONT
set, e.g.:
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);
}
That keeps the fix focused on not creating new invalid contpte entries,
while still catching a bad direct caller instead of silently sanitizing it.
Thanks,
Zhao
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 6:31 ` Zhao Gongyi
@ 2026-10-14 6:46 ` Lance Yang
2026-10-14 8:38 ` Baolin Wang
2026-10-14 10:03 ` Dev Jain
1 sibling, 1 reply; 17+ messages in thread
From: Lance Yang @ 2026-10-14 6:46 UTC (permalink / raw)
To: Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, dev.jain, baolin.wang,
mark.rutland, akpm, david, ljs, liam, vbabka, rppt, surenb,
mhocko, linux-arm-kernel, linux-mm, linux-kernel, stable
On 2026/10/14 14:31, Zhao Gongyi wrote:
> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>
>>> + /*
>>> + * Keep the producer-side invariant documented in pgtable.h true even
>>> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
>>> + */
>>> + if (!pte_valid(pte))
>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>
>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>> before we get here ... no? Why not fold !pte_valid(pte) into the
>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>
>> if (!mm_is_user(mm) || !pte_valid(pte))
>> return __set_ptes(mm, addr, ptep, pte, nr);
>>
>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
>> hide a caller bug :)
>>
>
> Hi Lance,
>
> Thanks for taking a look.
Cheers!
> Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
> path is unnecessary for the current in-tree path.
>
> The issue I was trying to avoid is not that the incoming pte already has
> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
> and then adds PTE_CONT again based on alignment. For an invalid descriptor,
> that transparent contiguous encoding should be skipped entirely.
>
> So your suggested shape makes sense to me. I will change this to return
> through __set_ptes() directly for invalid descriptors, and add a warning
> for any unexpected direct caller that passes an invalid PTE with PTE_CONT
> set, e.g.:
>
> 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);
> }
>
> That keeps the fix focused on not creating new invalid contpte entries,
> while still catching a bad direct caller instead of silently sanitizing it.
Thanks for the quick fix! Nothing jumped out at me :)
With that:
Tested-by: Lance Yang <lance.yang@linux.dev>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 6:46 ` Lance Yang
@ 2026-10-14 8:38 ` Baolin Wang
2026-10-14 9:24 ` Lance Yang
0 siblings, 1 reply; 17+ messages in thread
From: Baolin Wang @ 2026-10-14 8:38 UTC (permalink / raw)
To: Lance Yang, Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, dev.jain, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 10/14/26 2:46 PM, Lance Yang wrote:
>
>
> On 2026/10/14 14:31, Zhao Gongyi wrote:
>> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>>
>>>> + /*
>>>> + * Keep the producer-side invariant documented in pgtable.h
>>>> true even
>>>> + * if a direct caller passes an invalid descriptor with
>>>> PTE_CONT set.
>>>> + */
>>>> + if (!pte_valid(pte))
>>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>>
>>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>>> before we get here ... no? Why not fold !pte_valid(pte) into the
>>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>>
>>> if (!mm_is_user(mm) || !pte_valid(pte))
>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>>
>>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
>>> hide a caller bug :)
>>>
>>
>> Hi Lance,
>>
>> Thanks for taking a look.
>
> Cheers!
>
>> Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
>> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
>> path is unnecessary for the current in-tree path.
>>
>> The issue I was trying to avoid is not that the incoming pte already has
>> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
>> and then adds PTE_CONT again based on alignment. For an invalid
>> descriptor,
>> that transparent contiguous encoding should be skipped entirely.
>>
>> So your suggested shape makes sense to me. I will change this to return
>> through __set_ptes() directly for invalid descriptors, and add a warning
>> for any unexpected direct caller that passes an invalid PTE with PTE_CONT
>> set, e.g.:
>>
>> if (!mm_is_user(mm) || !pte_valid(pte)) {
>> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
mprotect() can trigger the warning here when setting PAGE_NONE for a
batch of PTEs?
>> return __set_ptes(mm, addr, ptep, pte, nr);
I am trying to convince myself that the 'nr' passed in is always
CONT_PTE_SIZE aligned, but it seems OK.
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 8:38 ` Baolin Wang
@ 2026-10-14 9:24 ` Lance Yang
2026-10-14 9:35 ` Baolin Wang
0 siblings, 1 reply; 17+ messages in thread
From: Lance Yang @ 2026-10-14 9:24 UTC (permalink / raw)
To: Baolin Wang, Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, dev.jain, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 2026/10/14 16:38, Baolin Wang wrote:
>
>
> On 10/14/26 2:46 PM, Lance Yang wrote:
>>
>>
>> On 2026/10/14 14:31, Zhao Gongyi wrote:
>>> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>>>
>>>>> + /*
>>>>> + * Keep the producer-side invariant documented in pgtable.h
>>>>> true even
>>>>> + * if a direct caller passes an invalid descriptor with
>>>>> PTE_CONT set.
>>>>> + */
>>>>> + if (!pte_valid(pte))
>>>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>>>
>>>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>>>> before we get here ... no? Why not fold !pte_valid(pte) into the
>>>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>>>
>>>> if (!mm_is_user(mm) || !pte_valid(pte))
>>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>>>
>>>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>>>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit
>>>> would
>>>> hide a caller bug :)
>>>>
>>>
>>> Hi Lance,
>>>
>>> Thanks for taking a look.
>>
>> Cheers!
>>
>>> Yes, you are right. The public set_ptes() wrapper already strips
>>> PTE_CONT
>>> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
>>> path is unnecessary for the current in-tree path.
>>>
>>> The issue I was trying to avoid is not that the incoming pte already has
>>> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
>>> and then adds PTE_CONT again based on alignment. For an invalid
>>> descriptor,
>>> that transparent contiguous encoding should be skipped entirely.
>>>
>>> So your suggested shape makes sense to me. I will change this to return
>>> through __set_ptes() directly for invalid descriptors, and add a warning
>>> for any unexpected direct caller that passes an invalid PTE with
>>> PTE_CONT
>>> set, e.g.:
>>>
>>> if (!mm_is_user(mm) || !pte_valid(pte)) {
>>> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
>
> mprotect() can trigger the warning here when setting PAGE_NONE for a
> batch of PTEs?
Nope. set_ptes() clears PTE_CONT before calling contpte_set_ptes().
PAGE_NONE makes !pte_valid(pte) true, but pte_cont(pte) is false,
so the warning won't fire :)
>>> return __set_ptes(mm, addr, ptep, pte, nr);
>
> I am trying to convince myself that the 'nr' passed in is always
> CONT_PTE_SIZE aligned, but it seems OK.
Still nope :)
IIRC, any partially covered contpte block should already be unfolded
by the time we get here ...
We're just installing PTEs without PTE_CONT into cleared entries,
so nr doesn't need to be aligned to CONT_PTES.
Cheers, Lance
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 9:24 ` Lance Yang
@ 2026-10-14 9:35 ` Baolin Wang
0 siblings, 0 replies; 17+ messages in thread
From: Baolin Wang @ 2026-10-14 9:35 UTC (permalink / raw)
To: Lance Yang, Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, dev.jain, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 10/14/26 5:24 PM, Lance Yang wrote:
>
>
> On 2026/10/14 16:38, Baolin Wang wrote:
>>
>>
>> On 10/14/26 2:46 PM, Lance Yang wrote:
>>>
>>>
>>> On 2026/10/14 14:31, Zhao Gongyi wrote:
>>>> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>>>>
>>>>>> + /*
>>>>>> + * Keep the producer-side invariant documented in pgtable.h
>>>>>> true even
>>>>>> + * if a direct caller passes an invalid descriptor with
>>>>>> PTE_CONT set.
>>>>>> + */
>>>>>> + if (!pte_valid(pte))
>>>>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>>>>
>>>>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>>>>> before we get here ... no? Why not fold !pte_valid(pte) into the
>>>>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>>>>
>>>>> if (!mm_is_user(mm) || !pte_valid(pte))
>>>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>>>>
>>>>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>>>>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit
>>>>> would
>>>>> hide a caller bug :)
>>>>>
>>>>
>>>> Hi Lance,
>>>>
>>>> Thanks for taking a look.
>>>
>>> Cheers!
>>>
>>>> Yes, you are right. The public set_ptes() wrapper already strips
>>>> PTE_CONT
>>>> before calling contpte_set_ptes(), so the pte_mknoncont() in the
>>>> invalid
>>>> path is unnecessary for the current in-tree path.
>>>>
>>>> The issue I was trying to avoid is not that the incoming pte already
>>>> has
>>>> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/
>>>> prot
>>>> and then adds PTE_CONT again based on alignment. For an invalid
>>>> descriptor,
>>>> that transparent contiguous encoding should be skipped entirely.
>>>>
>>>> So your suggested shape makes sense to me. I will change this to return
>>>> through __set_ptes() directly for invalid descriptors, and add a
>>>> warning
>>>> for any unexpected direct caller that passes an invalid PTE with
>>>> PTE_CONT
>>>> set, e.g.:
>>>>
>>>> if (!mm_is_user(mm) || !pte_valid(pte)) {
>>>> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
>>
>> mprotect() can trigger the warning here when setting PAGE_NONE for a
>> batch of PTEs?
>
> Nope. set_ptes() clears PTE_CONT before calling contpte_set_ptes().
> PAGE_NONE makes !pte_valid(pte) true, but pte_cont(pte) is false,
> so the warning won't fire :)
Ah, right.
>>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>
>> I am trying to convince myself that the 'nr' passed in is always
>> CONT_PTE_SIZE aligned, but it seems OK.
>
> Still nope :)
>
> IIRC, any partially covered contpte block should already be unfolded
> by the time we get here ...
>
> We're just installing PTEs without PTE_CONT into cleared entries,
> so nr doesn't need to be aligned to CONT_PTES.
OK. I see. Thanks for explanation. So looks good to me.
Reviewed-by: Baolin Wang <baolin.wang@linux.alibaba.com>
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 6:31 ` Zhao Gongyi
2026-10-14 6:46 ` Lance Yang
@ 2026-10-14 10:03 ` Dev Jain
2026-10-14 10:45 ` Lance Yang
1 sibling, 1 reply; 17+ messages in thread
From: Dev Jain @ 2026-10-14 10:03 UTC (permalink / raw)
To: Zhao Gongyi, Lance Yang
Cc: catalin.marinas, will, ryan.roberts, baolin.wang, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 14/10/26 12:01 pm, Zhao Gongyi wrote:
> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>
>>> + /*
>>> + * Keep the producer-side invariant documented in pgtable.h true even
>>> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
>>> + */
>>> + if (!pte_valid(pte))
>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>
>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>> before we get here ... no? Why not fold !pte_valid(pte) into the
>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>
>> if (!mm_is_user(mm) || !pte_valid(pte))
>> return __set_ptes(mm, addr, ptep, pte, nr);
>>
>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
>> hide a caller bug :)
>>
>
> Hi Lance,
>
> Thanks for taking a look.
>
> Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
> path is unnecessary for the current in-tree path.
>
> The issue I was trying to avoid is not that the incoming pte already has
> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
> and then adds PTE_CONT again based on alignment. For an invalid descriptor,
> that transparent contiguous encoding should be skipped entirely.
>
> So your suggested shape makes sense to me. I will change this to return
> through __set_ptes() directly for invalid descriptors, and add a warning
> for any unexpected direct caller that passes an invalid PTE with PTE_CONT
> set, e.g.:
>
> if (!mm_is_user(mm) || !pte_valid(pte)) {
> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
set_ptes does pte_mknoncont(), so the warning will *never* fire I think?
> return __set_ptes(mm, addr, ptep, pte, nr);
> }
>
> That keeps the fix focused on not creating new invalid contpte entries,
> while still catching a bad direct caller instead of silently sanitizing it.
>
> Thanks,
> Zhao
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 10:03 ` Dev Jain
@ 2026-10-14 10:45 ` Lance Yang
2026-10-14 11:02 ` Dev Jain
0 siblings, 1 reply; 17+ messages in thread
From: Lance Yang @ 2026-10-14 10:45 UTC (permalink / raw)
To: Dev Jain, Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, baolin.wang, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 2026/10/14 18:03, Dev Jain wrote:
>
>
> On 14/10/26 12:01 pm, Zhao Gongyi wrote:
>> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>>
>>>> + /*
>>>> + * Keep the producer-side invariant documented in pgtable.h true even
>>>> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
>>>> + */
>>>> + if (!pte_valid(pte))
>>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>>
>>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>>> before we get here ... no? Why not fold !pte_valid(pte) into the
>>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>>
>>> if (!mm_is_user(mm) || !pte_valid(pte))
>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>>
>>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
>>> hide a caller bug :)
>>>
>>
>> Hi Lance,
>>
>> Thanks for taking a look.
>>
>> Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
>> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
>> path is unnecessary for the current in-tree path.
>>
>> The issue I was trying to avoid is not that the incoming pte already has
>> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
>> and then adds PTE_CONT again based on alignment. For an invalid descriptor,
>> that transparent contiguous encoding should be skipped entirely.
>>
>> So your suggested shape makes sense to me. I will change this to return
>> through __set_ptes() directly for invalid descriptors, and add a warning
>> for any unexpected direct caller that passes an invalid PTE with PTE_CONT
>> set, e.g.:
>>
>> if (!mm_is_user(mm) || !pte_valid(pte)) {
>> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
>
> set_ptes does pte_mknoncont(), so the warning will *never* fire I think?
YES. Just a sanity check to catch a future direct caller passing
a !pte_valid PTE with PTE_CONT set, rather than silently clearing
the bit :)
Cheers, Lance
>
>> return __set_ptes(mm, addr, ptep, pte, nr);
>> }
>>
>> That keeps the fix focused on not creating new invalid contpte entries,
>> while still catching a bad direct caller instead of silently sanitizing it.
>>
>> Thanks,
>> Zhao
>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 10:45 ` Lance Yang
@ 2026-10-14 11:02 ` Dev Jain
0 siblings, 0 replies; 17+ messages in thread
From: Dev Jain @ 2026-10-14 11:02 UTC (permalink / raw)
To: Lance Yang, Zhao Gongyi
Cc: catalin.marinas, will, ryan.roberts, baolin.wang, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, stable
On 14/10/26 4:15 pm, Lance Yang wrote:
>
>
> On 2026/10/14 18:03, Dev Jain wrote:
>>
>>
>> On 14/10/26 12:01 pm, Zhao Gongyi wrote:
>>> On 10/13/26 11:39 PM, Lance Yang wrote:
>>>>>
>>>>> + /*
>>>>> + * Keep the producer-side invariant documented in pgtable.h true even
>>>>> + * if a direct caller passes an invalid descriptor with PTE_CONT set.
>>>>> + */
>>>>> + if (!pte_valid(pte))
>>>>> + return __set_ptes(mm, addr, ptep, pte_mknoncont(pte), nr);
>>>>
>>>> set_ptes() is the only in-tree caller, and it already clears PTE_CONT
>>>> before we get here ... no? Why not fold !pte_valid(pte) into the
>>>> !mm_is_user(mm) check below and return __set_ptes() directly?
>>>>
>>>> if (!mm_is_user(mm) || !pte_valid(pte))
>>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>>>
>>>> If a direct caller does pass a !pte_valid PTE with PTE_CONT set, I'd
>>>> rather add a VM_WARN_ON() to catch that. Silently clearing the bit would
>>>> hide a caller bug :)
>>>>
>>>
>>> Hi Lance,
>>>
>>> Thanks for taking a look.
>>>
>>> Yes, you are right. The public set_ptes() wrapper already strips PTE_CONT
>>> before calling contpte_set_ptes(), so the pte_mknoncont() in the invalid
>>> path is unnecessary for the current in-tree path.
>>>
>>> The issue I was trying to avoid is not that the incoming pte already has
>>> PTE_CONT set, but that contpte_set_ptes() rebuilds the pte from pfn/prot
>>> and then adds PTE_CONT again based on alignment. For an invalid descriptor,
>>> that transparent contiguous encoding should be skipped entirely.
>>>
>>> So your suggested shape makes sense to me. I will change this to return
>>> through __set_ptes() directly for invalid descriptors, and add a warning
>>> for any unexpected direct caller that passes an invalid PTE with PTE_CONT
>>> set, e.g.:
>>>
>>> if (!mm_is_user(mm) || !pte_valid(pte)) {
>>> VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte));
>>
>> set_ptes does pte_mknoncont(), so the warning will *never* fire I think?
>
> YES. Just a sanity check to catch a future direct caller passing
> a !pte_valid PTE with PTE_CONT set, rather than silently clearing
> the bit :)
Ah okay you said "future direct caller". I got confused since if the only
caller is set_ptes() then the warning is useless by construction.
>
> Cheers, Lance
>
>>
>>> return __set_ptes(mm, addr, ptep, pte, nr);
>>> }
>>>
>>> That keeps the fix focused on not creating new invalid contpte entries,
>>> while still catching a bad direct caller instead of silently sanitizing it.
>>>
>>> Thanks,
>>> Zhao
>>
>
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 9:36 [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs Zhao Gongyi
2026-10-13 10:13 ` Dev Jain
2026-10-13 15:39 ` Lance Yang
@ 2026-10-14 9:41 ` David Hildenbrand (Arm)
2026-10-19 7:06 ` Zhao Gongyi
2026-10-23 12:12 ` [PATCH v2] " Zhao Gongyi
3 siblings, 1 reply; 17+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-14 9:41 UTC (permalink / raw)
To: Zhao Gongyi, catalin.marinas, will, Ryan Roberts
Cc: ryan.roberts, dev.jain, baolin.wang, lance.yang, mark.rutland,
akpm, ljs, liam, vbabka, rppt, surenb, mhocko, linux-arm-kernel,
linux-mm, linux-kernel, stable
CCing Ryan:
https://lore.kernel.org/r/20261013093600.54995-1-zhaogongyi@bytedance.com
On 10/13/26 11:36, Zhao Gongyi wrote:
> The transparent contpte layer treats PTE_CONT as software state: when a
> PTE has PTE_CONT set, the contpte helpers may align the operation to a
> CONT_PTE_SIZE block and operate on the whole block. That invariant only
> makes sense for valid mappings. 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 intentionally test
> pte_cont() directly instead of pte_valid_cont(), for example commit
> 9970a9a27ffc ("arm64: mm: implement the architecture-specific
> test_and_clear_young_ptes()"). Fixing the producer side preserves these
> fast paths and covers all consumers.
>
> 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.
Hm, is there a good reason why we cannot just use the cont-pte bit for all
*present* PTEs, even if HW would not be prepared to benefit (!pte_valid())?
I guess we really only have to make sure to shatter properly, right?
--
Cheers,
David
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-14 9:41 ` David Hildenbrand (Arm)
@ 2026-10-19 7:06 ` Zhao Gongyi
2026-10-23 20:55 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 17+ messages in thread
From: Zhao Gongyi @ 2026-10-19 7:06 UTC (permalink / raw)
To: David Hildenbrand (Arm), catalin.marinas, will, Ryan Roberts
Cc: dev.jain, baolin.wang, lance.yang, mark.rutland, akpm, ljs, liam,
vbabka, rppt, surenb, mhocko, linux-arm-kernel, linux-mm,
linux-kernel, stable
On 10/14/26 5:41 PM, David Hildenbrand (Arm) wrote:
> CCing Ryan:
>
> https://lore.kernel.org/r/20261013093600.54995-1-zhaogongyi@bytedance.com
>
> On 10/13/26 11:36, Zhao Gongyi wrote:
>> The transparent contpte layer treats PTE_CONT as software state: when a
>> PTE has PTE_CONT set, the contpte helpers may align the operation to a
>> CONT_PTE_SIZE block and operate on the whole block. That invariant only
>> makes sense for valid mappings. 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 intentionally test
>> pte_cont() directly instead of pte_valid_cont(), for example commit
>> 9970a9a27ffc ("arm64: mm: implement the architecture-specific
>> test_and_clear_young_ptes()"). Fixing the producer side preserves these
>> fast paths and covers all consumers.
>>
>> 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.
>
> Hm, is there a good reason why we cannot just use the cont-pte bit for all
> *present* PTEs, even if HW would not be prepared to benefit (!pte_valid())?
>
> I guess we really only have to make sure to shatter properly, right?
>
Hi David,
Right, there is no architectural restriction preventing PTE_CONT from
being used as software grouping state for present-invalid PTEs. For this
particular failure, making contpte_try_unfold() recognize something like
pte_present(pte) && pte_cont(pte)
would make clear_full_ptes(nr == 1) unfold the block before replacing one
entry with the guard marker.
My reason for preferring the producer-side fix is that the existing
transparent contpte helpers mostly use pte_valid_cont() as the membership
test, including contpte_try_unfold(), ptep_get[_lockless](),
pte_batch_hint() and contpte_is_consistent(), while some range fast paths
test pte_cont() directly.
If present-invalid PTEs are also considered members of a transparent
contpte block, I think we should audit the single-PTE modification paths,
rather than changing only this clear path. We would likely also need a
pte_present_cont() helper: the pte_present() guard matters because the
PTE_CONT bit position is part of the swap offset for non-present entries.
There is no hardware benefit while the descriptors are invalid, and
set_ptes() can recreate the contiguous encoding when an eligible range is
made valid again. For a stable fix, I therefore preferred to stop
contpte_set_ptes() from creating present-invalid transparent contpte
mappings, which also matches the pte_valid_cont() contract used by most
of the existing helpers.
I agree that saying this invariant "only makes sense" for valid mappings
is too strong. I will reword that to describe the current helper
contract instead.
If preserving the software grouping across PROT_NONE is the intended
semantics, I can rework this as a broader unfold-path audit instead.
Thanks,
Gongyi
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-19 7:06 ` Zhao Gongyi
@ 2026-10-23 20:55 ` David Hildenbrand (Arm)
2026-10-24 16:00 ` Zhao Gongyi
0 siblings, 1 reply; 17+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-23 20:55 UTC (permalink / raw)
To: Zhao Gongyi, catalin.marinas, will, Ryan Roberts
Cc: dev.jain, baolin.wang, lance.yang, mark.rutland, akpm, ljs, liam,
vbabka, rppt, surenb, mhocko, linux-arm-kernel, linux-mm,
linux-kernel, stable
On 10/19/26 09:06, Zhao Gongyi wrote:
> On 10/14/26 5:41 PM, David Hildenbrand (Arm) wrote:
>> CCing Ryan:
>>
>> https://lore.kernel.org/r/20261013093600.54995-1-zhaogongyi@bytedance.com
>>
>> On 10/13/26 11:36, Zhao Gongyi wrote:
>>> The transparent contpte layer treats PTE_CONT as software state: when a
>>> PTE has PTE_CONT set, the contpte helpers may align the operation to a
>>> CONT_PTE_SIZE block and operate on the whole block. That invariant only
>>> makes sense for valid mappings. 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 intentionally test
>>> pte_cont() directly instead of pte_valid_cont(), for example commit
>>> 9970a9a27ffc ("arm64: mm: implement the architecture-specific
>>> test_and_clear_young_ptes()"). Fixing the producer side preserves these
>>> fast paths and covers all consumers.
>>>
>>> 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.
>>
>> Hm, is there a good reason why we cannot just use the cont-pte bit for all
>> *present* PTEs, even if HW would not be prepared to benefit (!pte_valid())?
>>
>> I guess we really only have to make sure to shatter properly, right?
>>
>
>
> Hi David,
>
> Right, there is no architectural restriction preventing PTE_CONT from
> being used as software grouping state for present-invalid PTEs. For this
> particular failure, making contpte_try_unfold() recognize something like
>
> pte_present(pte) && pte_cont(pte)
>
> would make clear_full_ptes(nr == 1) unfold the block before replacing one
> entry with the guard marker.
>
> My reason for preferring the producer-side fix is that the existing
> transparent contpte helpers mostly use pte_valid_cont() as the membership
> test, including contpte_try_unfold(), ptep_get[_lockless](),
> pte_batch_hint() and contpte_is_consistent(), while some range fast paths
> test pte_cont() directly.
>
> If present-invalid PTEs are also considered members of a transparent
> contpte block, I think we should audit the single-PTE modification paths,
> rather than changing only this clear path. We would likely also need a
> pte_present_cont() helper: the pte_present() guard matters because the
> PTE_CONT bit position is part of the swap offset for non-present entries.
>
> There is no hardware benefit while the descriptors are invalid, and
> set_ptes() can recreate the contiguous encoding when an eligible range is
> made valid again. For a stable fix, I therefore preferred to stop
> contpte_set_ptes() from creating present-invalid transparent contpte
> mappings, which also matches the pte_valid_cont() contract used by most
> of the existing helpers.
>
> I agree that saying this invariant "only makes sense" for valid mappings
> is too strong. I will reword that to describe the current helper
> contract instead.
>
> If preserving the software grouping across PROT_NONE is the intended
> semantics, I can rework this as a broader unfold-path audit instead.
Hi,
sorry for the late reply.
If we have the spare PTE bit, why shouldn't we just use it?
It will make batching over PTEs faster. And we care about that for present PTEs.
So I don't quite see why the stable fix couldn't just do it right away? Do you
have a prototype that shows that it would require significant code changes?
--
Cheers,
David
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-23 20:55 ` David Hildenbrand (Arm)
@ 2026-10-24 16:00 ` Zhao Gongyi
0 siblings, 0 replies; 17+ messages in thread
From: Zhao Gongyi @ 2026-10-24 16:00 UTC (permalink / raw)
To: David Hildenbrand (Arm), catalin.marinas, will, Ryan Roberts
Cc: dev.jain, baolin.wang, lance.yang, mark.rutland, akpm, ljs, liam,
vbabka, rppt, surenb, mhocko, linux-arm-kernel, linux-mm,
linux-kernel, stable
On 10/24/26 4:55 AM, David Hildenbrand (Arm) wrote:
>
> Hi,
>
> sorry for the late reply.
>
> If we have the spare PTE bit, why shouldn't we just use it?
>
> It will make batching over PTEs faster. And we care about that for present PTEs.
>
> So I don't quite see why the stable fix couldn't just do it right away? Do you
> have a prototype that shows that it would require significant code changes?
>
Hi David,
I tried a small prototype that keeps PTE_CONT on present-invalid PTEs.
It leaves contpte_set_ptes() unchanged and makes contpte_try_unfold()
recognize pte_present(pte) && pte_cont(pte), instead of checking only
pte_valid_cont(pte).
With this change, the guard-marker reproducer passed 100 iterations
in an arm64 QEMU/KVM guest running a KASAN-enabled kernel. The fork
and PMD-split probes passed five runs each, with no warnings or KASAN
reports. In the earlier A/B test, the unmodified kernel failed on the
first guard-marker iteration.
The prototype makes the reported reproducer pass with a small change
while retaining PTE_CONT. I therefore cannot point to evidence that
fixing this failure requires significant code changes.
Thanks,
Gongyi
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2] arm64/mm: restrict transparent contpte mappings to valid PTEs
2026-10-13 9:36 [PATCH] arm64/mm: restrict transparent contpte mappings to valid PTEs Zhao Gongyi
` (2 preceding siblings ...)
2026-10-14 9:41 ` David Hildenbrand (Arm)
@ 2026-10-23 12:12 ` Zhao Gongyi
3 siblings, 0 replies; 17+ messages in thread
From: Zhao Gongyi @ 2026-10-23 12:12 UTC (permalink / raw)
To: catalin.marinas, will
Cc: ryan.roberts, dev.jain, baolin.wang, lance.yang, mark.rutland,
akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
linux-arm-kernel, linux-mm, linux-kernel, Zhao Gongyi, stable
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 <baolin.wang@linux.alibaba.com>
Tested-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
---
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)
^ permalink raw reply [flat|nested] 17+ messages in thread