* [PATCH mm-hotfixes v4 1/4] mm/vmalloc: acquire init_mm lock on huge vmap to avoid ptdump UAF
2026-07-16 21:31 [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Lorenzo Stoakes (ARM)
@ 2026-07-16 21:31 ` Lorenzo Stoakes (ARM)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF Lorenzo Stoakes (ARM)
` (3 subsequent siblings)
4 siblings, 0 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-16 21:31 UTC (permalink / raw)
To: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts
Cc: David Carlier, ljs, linux-mm, linux-kernel, bpf, linux-arm-kernel,
Denis V. Lunev, stable, syzbot+fd95a72470f5a44e464c
Currently there is a nasty race between ptdump and vmap when attempting to
map a huge P4D, PUD or PMD entry:
* ptdump walks kernel page table ranges it doesn't own.
* When vmap maps ranges it tries to promotes existing ones to huge page
tables in vmap_try_huge_[p4d,pud,pmd]() at P4D, PUD and PMD level,
freeing the lower page table in [p4d,pud,pmd]_free_[pud,pmd,pte]_page()
when it succeeds.
Both of these things can happen at the same time and as a result ptdump can
access a freed page table, resulting in a use-after-free and memory
corruption.
This is possible because while ptdump_walk_pgd() holds both the mem hotplug
lock and the mmap write lock before invoking walk_page_range_debug(), vmap
takes no relevant locks at all.
Fix this by holding the mmap read lock in vmap_try_huge_*() when freeing
page tables.
The read lock is sufficient: ptdump is the only walker that must be
excluded and it holds the mmap write lock. Other holders of the read lock
may run concurrently, but each exclusively owns the range it operates on
and cannot reach the page tables freed here.
We also hold the lock while assigning the huge page table entry, which
means page table walkers observe only the huge or non-huge page table
entry.
We use a trylock to prevent ptdump from blocking vmap making forward
progress. This is fine because it's an optimisation in any case, and thus
the vmap can safely proceed regardless.
All other kernel page table walkers that touch vmalloc ranges either
exclusively own the memory walked or acquire the mmap lock, so this
correctly excludes those walkers.
One wrinkle here is commit fa93b45fd397 ("arm64: Enable vmalloc-huge with
ptdump"), which addresses the issue for arm64 only by explicitly acquiring
the mmap read lock on kernel page table freeing should a concurrent ptdump
be in progress.
This is problematic as vmap may acquire the mmap read lock prior to ptdump
attempting to acquire an mmap write lock, leading to a deadlock when the
mmap read lock is slept upon on page table freeing due to rwsem
anti-starvation.
We work around this by predicating the mmap lock being taken on
!CONFIG_ARM64 for the time being.
With this patch applied, a follow up will partially revert commit
fa93b45fd397 ("arm64: Enable vmalloc-huge with ptdump") and at that stage
remove the arm64 ifdeffery.
We also update walk_page_range_debug() to assert the mmap write lock
unconditionally and update the comment here to reflect this change.
The issue has existed as long as ptdump was available and vmap freed page
tables when promoting to a huge leaf entry, that is, since commit
b6bdb7517c3d ("mm/vmalloc: add interfaces to free unmapped page table") for
huge ioremap, and commit 121e6f3258fe ("mm/vmalloc: hugepage vmalloc
mappings") for huge vmalloc.
Since the former is the earlier of the two we choose that for our Fixes
tag.
We also define a guard class for mmap_read_trylock() so we can use
cleanup.h to make the scope handling cleaner in the implementation.
This patch is based on work by David Carlier (linked), with gratitude!
Fixes: b6bdb7517c3d ("mm/vmalloc: add interfaces to free unmapped page table")
Cc: stable@vger.kernel.org
Reported-by: syzbot+fd95a72470f5a44e464c@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/all/6a287988.39669fcc.33b062.00a0.GAE@google.com/T/
Link: https://lore.kernel.org/linux-mm/20260706203128.162335-1-devnexen@gmail.com/
Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Reviewed-by: Dev Jain <dev.jain@arm.com>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
include/linux/mmap_lock.h | 1 +
mm/pagewalk.c | 22 +++++++++++----------
mm/vmalloc.c | 49 ++++++++++++++++++++++++++++++++++++++---------
3 files changed, 53 insertions(+), 19 deletions(-)
diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
index 04b8f61ece5d..6b5c2390cc30 100644
--- a/include/linux/mmap_lock.h
+++ b/include/linux/mmap_lock.h
@@ -621,6 +621,7 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
mmap_read_lock(_T), mmap_read_unlock(_T))
+DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
{
diff --git a/mm/pagewalk.c b/mm/pagewalk.c
index 3ae2586ff45b..bbcfd68d0907 100644
--- a/mm/pagewalk.c
+++ b/mm/pagewalk.c
@@ -678,6 +678,8 @@ int walk_kernel_page_table_range_lockless(unsigned long start, unsigned long end
* will also not lock the PTEs for the pte_entry() callback.
*
* This is for debugging purposes ONLY.
+ *
+ * The mmap write lock must be held.
*/
int walk_page_range_debug(struct mm_struct *mm, unsigned long start,
unsigned long end, const struct mm_walk_ops *ops,
@@ -691,6 +693,16 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start,
.no_vma = true
};
+ /*
+ * When walking userland page tables, an mmap write lock must be held to
+ * account for munmap() downgrading to an mmap read lock when tearing
+ * down page tables.
+ *
+ * When walking kernel page tables, an mmap write lock must also be held
+ * to account for page table freeing on vmap huge page mapping.
+ */
+ mmap_assert_write_locked(mm);
+
/* For convenience, we allow traversal of kernel mappings. */
if (mm == &init_mm)
return walk_kernel_page_table_range(start, end, ops,
@@ -700,16 +712,6 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start,
if (!check_ops_safe(ops))
return -EINVAL;
- /*
- * The mmap lock protects the page walker from changes to the page
- * tables during the walk. However a read lock is insufficient to
- * protect those areas which don't have a VMA as munmap() detaches
- * the VMAs before downgrading to a read lock and actually tearing
- * down PTEs/page tables. In which case, the mmap write lock should
- * be held.
- */
- mmap_assert_write_locked(mm);
-
return walk_pgd_range(start, end, &walk);
}
diff --git a/mm/vmalloc.c b/mm/vmalloc.c
index 1afca3568b9b..d5c4d2bb770b 100644
--- a/mm/vmalloc.c
+++ b/mm/vmalloc.c
@@ -43,6 +43,7 @@
#include <asm/tlbflush.h>
#include <asm/shmparam.h>
#include <linux/page_owner.h>
+#include <linux/cleanup.h>
#define CREATE_TRACE_POINTS
#include <trace/events/vmalloc.h>
@@ -158,10 +159,24 @@ static int vmap_try_huge_pmd(pmd_t *pmd, unsigned long addr, unsigned long end,
if (!IS_ALIGNED(phys_addr, PMD_SIZE))
return 0;
- if (pmd_present(*pmd) && !pmd_free_pte_page(pmd, addr))
- return 0;
+ if (!pmd_present(*pmd))
+ return pmd_set_huge(pmd, phys_addr, prot);
- return pmd_set_huge(pmd, phys_addr, prot);
+ /*
+ * Acquire the mmap read lock to exclude ptdump, which walks
+ * kernel page tables it does not own under the mmap write lock.
+ *
+ * Concurrent read lock holders are safe: each exclusively owns
+ * the range it operates on and cannot reach this page table.
+ */
+#ifndef CONFIG_ARM64
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
+#endif
+ {
+ if (!pmd_free_pte_page(pmd, addr))
+ return 0;
+ return pmd_set_huge(pmd, phys_addr, prot);
+ }
}
static int vmap_pmd_range(pud_t *pud, unsigned long addr, unsigned long end,
@@ -210,10 +225,18 @@ static int vmap_try_huge_pud(pud_t *pud, unsigned long addr, unsigned long end,
if (!IS_ALIGNED(phys_addr, PUD_SIZE))
return 0;
- if (pud_present(*pud) && !pud_free_pmd_page(pud, addr))
- return 0;
+ if (!pud_present(*pud))
+ return pud_set_huge(pud, phys_addr, prot);
- return pud_set_huge(pud, phys_addr, prot);
+ /* See comment in vmap_try_huge_pmd(). */
+#ifndef CONFIG_ARM64
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
+#endif
+ {
+ if (!pud_free_pmd_page(pud, addr))
+ return 0;
+ return pud_set_huge(pud, phys_addr, prot);
+ }
}
static int vmap_pud_range(p4d_t *p4d, unsigned long addr, unsigned long end,
@@ -262,10 +285,18 @@ static int vmap_try_huge_p4d(p4d_t *p4d, unsigned long addr, unsigned long end,
if (!IS_ALIGNED(phys_addr, P4D_SIZE))
return 0;
- if (p4d_present(*p4d) && !p4d_free_pud_page(p4d, addr))
- return 0;
+ if (!p4d_present(*p4d))
+ return p4d_set_huge(p4d, phys_addr, prot);
- return p4d_set_huge(p4d, phys_addr, prot);
+ /* See comment in vmap_try_huge_pmd(). */
+#ifndef CONFIG_ARM64
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
+#endif
+ {
+ if (!p4d_free_pud_page(p4d, addr))
+ return 0;
+ return p4d_set_huge(p4d, phys_addr, prot);
+ }
}
static int vmap_p4d_range(pgd_t *pgd, unsigned long addr, unsigned long end,
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-16 21:31 [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Lorenzo Stoakes (ARM)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 1/4] mm/vmalloc: acquire init_mm lock on huge vmap to avoid ptdump UAF Lorenzo Stoakes (ARM)
@ 2026-07-16 21:31 ` Lorenzo Stoakes (ARM)
2026-07-17 3:46 ` David CARLIER
` (2 more replies)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 3/4] mm/ptdump: always stabilise against page table freeing using init_mm Lorenzo Stoakes (ARM)
` (2 subsequent siblings)
4 siblings, 3 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-16 21:31 UTC (permalink / raw)
To: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts
Cc: David Carlier, ljs, linux-mm, linux-kernel, bpf, linux-arm-kernel,
Denis V. Lunev, stable
x86 implements page attribute modification using its Change Page
Attributes (CPA) mechanism.
This tracks properties of ranges such as cache mode through x86 page
attributes, and as part of that logic manipulates kernel page tables.
Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
fragmentation") ranges of kernel page table entries can be collapsed into
huge page table entries as part of this logic.
As part of this collapse, it frees the page tables which the collapsed
entries previously pointed to, and it does so without any relevant locks
being held to preclude concurrent kernel page table walkers.
The only way this code can be reached is if CPA_COLLAPSE is specified, and
this is only set in set_memory_rox() via:
set_memory_rox()
-> change_page_attr_set_clr()
-> cpa_flush()
-> cpa_collapse_large_pages()
Notable users of this are execmem and bpf when manipulating executable
mappings.
However, this is problematic for ptdump as it walks ranges it does not own
and thus runs the risk of a use-after-free on page tables freed underneath
it.
In addition, concurrent CPA collapse operations are possible which can also
cause races.
Resolve the issue by acquiring the mmap write lock on init_mm across the
whole operation.
It is safe to acquire a sleeping lock as all the callers invoke
set_memory_rox() from process context and in any case,
change_page_attr_set_clr() calls vm_unmap_alias() which ultimately takes a
mutex, disallowing atomic context here.
Fixes: 41d88484c71c ("x86/mm/pat: restore large ROX pages after fragmentation")
Cc: stable@vger.kernel.org
Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Dave Hansen <dave.hansen@linux.intel.com>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
arch/x86/mm/pat/set_memory.c | 15 ++++++++++++++-
include/linux/mmap_lock.h | 2 ++
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
index d023a40a1e03..d1e63f7d267f 100644
--- a/arch/x86/mm/pat/set_memory.c
+++ b/arch/x86/mm/pat/set_memory.c
@@ -22,6 +22,7 @@
#include <linux/cc_platform.h>
#include <linux/set_memory.h>
#include <linux/memregion.h>
+#include <linux/cleanup.h>
#include <asm/e820/api.h>
#include <asm/processor.h>
@@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
-static void cpa_collapse_large_pages(struct cpa_data *cpa)
+static void __cpa_collapse_large_pages(struct cpa_data *cpa)
{
unsigned long start, addr, end;
struct ptdesc *ptdesc, *tmp;
@@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
}
}
+static void cpa_collapse_large_pages(struct cpa_data *cpa)
+{
+ /*
+ * Take the mmap write lock on init_mm to:
+ * - Avoid a use-after-free if raced by ptdump (which takes its own
+ * write lock on init_mm).
+ * - Serialise concurrent CPA walkers.
+ */
+ scoped_guard(mmap_write_lock, &init_mm)
+ __cpa_collapse_large_pages(cpa);
+}
+
static void cpa_flush(struct cpa_data *cpa, int cache)
{
unsigned int i;
diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
index 6b5c2390cc30..047f5f5e2c34 100644
--- a/include/linux/mmap_lock.h
+++ b/include/linux/mmap_lock.h
@@ -621,6 +621,8 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
mmap_read_lock(_T), mmap_read_unlock(_T))
+DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
+ mmap_write_lock(_T), mmap_write_unlock(_T))
DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF Lorenzo Stoakes (ARM)
@ 2026-07-17 3:46 ` David CARLIER
2026-07-17 7:47 ` Lorenzo Stoakes (ARM)
2026-07-17 11:30 ` Will Deacon
2026-07-17 11:40 ` Lorenzo Stoakes (ARM)
2 siblings, 1 reply; 12+ messages in thread
From: David CARLIER @ 2026-07-17 3:46 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable
On Thu, 16 Jul 2026 at 22:31, Lorenzo Stoakes (ARM) <ljs@kernel.org> wrote:
>
> x86 implements page attribute modification using its Change Page
> Attributes (CPA) mechanism.
>
> This tracks properties of ranges such as cache mode through x86 page
> attributes, and as part of that logic manipulates kernel page tables.
>
> Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> fragmentation") ranges of kernel page table entries can be collapsed into
> huge page table entries as part of this logic.
>
> As part of this collapse, it frees the page tables which the collapsed
> entries previously pointed to, and it does so without any relevant locks
> being held to preclude concurrent kernel page table walkers.
>
> The only way this code can be reached is if CPA_COLLAPSE is specified, and
> this is only set in set_memory_rox() via:
>
> set_memory_rox()
> -> change_page_attr_set_clr()
> -> cpa_flush()
> -> cpa_collapse_large_pages()
>
> Notable users of this are execmem and bpf when manipulating executable
> mappings.
>
> However, this is problematic for ptdump as it walks ranges it does not own
> and thus runs the risk of a use-after-free on page tables freed underneath
> it.
>
> In addition, concurrent CPA collapse operations are possible which can also
> cause races.
>
> Resolve the issue by acquiring the mmap write lock on init_mm across the
> whole operation.
>
> It is safe to acquire a sleeping lock as all the callers invoke
> set_memory_rox() from process context and in any case,
> change_page_attr_set_clr() calls vm_unmap_alias() which ultimately takes a
> mutex, disallowing atomic context here.
>
> Fixes: 41d88484c71c ("x86/mm/pat: restore large ROX pages after fragmentation")
> Cc: stable@vger.kernel.org
> Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> Reviewed-by: Dave Hansen <dave.hansen@linux.intel.com>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> ---
> arch/x86/mm/pat/set_memory.c | 15 ++++++++++++++-
> include/linux/mmap_lock.h | 2 ++
> 2 files changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> index d023a40a1e03..d1e63f7d267f 100644
> --- a/arch/x86/mm/pat/set_memory.c
> +++ b/arch/x86/mm/pat/set_memory.c
> @@ -22,6 +22,7 @@
> #include <linux/cc_platform.h>
> #include <linux/set_memory.h>
> #include <linux/memregion.h>
> +#include <linux/cleanup.h>
>
> #include <asm/e820/api.h>
> #include <asm/processor.h>
> @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
>
> static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
>
> -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> {
> unsigned long start, addr, end;
> struct ptdesc *ptdesc, *tmp;
> @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> }
> }
>
> +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +{
> + /*
> + * Take the mmap write lock on init_mm to:
> + * - Avoid a use-after-free if raced by ptdump (which takes its own
> + * write lock on init_mm).
> + * - Serialise concurrent CPA walkers.
> + */
> + scoped_guard(mmap_write_lock, &init_mm)
> + __cpa_collapse_large_pages(cpa);
> +}
> +
> static void cpa_flush(struct cpa_data *cpa, int cache)
> {
> unsigned int i;
> diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
> index 6b5c2390cc30..047f5f5e2c34 100644
> --- a/include/linux/mmap_lock.h
> +++ b/include/linux/mmap_lock.h
> @@ -621,6 +621,8 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
>
> DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
> mmap_read_lock(_T), mmap_read_unlock(_T))
> +DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
> + mmap_write_lock(_T), mmap_write_unlock(_T))
> DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
>
> static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
>
> --
> 2.55.0
>
Hi,
This bullet confuses me. __change_page_attr() and friends are serialised by
cpa_lock/pgd_lock and never take init_mm's mmap lock, so this doesn't
exclude them.
Collapse vs collapse looks covered already: collapse_large_pages() holds
pgd_lock across the walk and both collapse_pmd_page()/collapse_pud_page()
calls, and the pmd_leaf()/pud_leaf() early-outs stop a second CPU
re-queueing a collapsed table. I couldn't build the interleaving behind the
cover letter's "could result in memory corruption". Can you spell it out?
If the reason is really Will's v3 point - collapse rounds up to PMD/PUD and
frees tables outside its caller's range, so it has to exclude
walk_kernel_page_table_range() walkers holding the read lock - then say
that. It's a UAF independent of ptdump, and it's why v3's read lock wasn't
enough.
Also: patch 1 justifies the trylock fallback on ptdump being rare, but this
takes the write lock on every set_memory_rox() (BPF JIT, module load,
execmem), even when nothing collapsed. vmap_try_huge_*() then loses the
trylock and quietly falls back to PTEs on every BPF load. No counter, no
trace. Not a blocker and I've no better idea, but fa93b45fd397 was about
getting vmalloc-huge on by default, so it's worth calling out.
Rest looks right. 4/4 restores pud_free_pmd_page() exactly as it was before
fa93b45fd397, and 3/4 builds the same walk init_mm got via
walk_kernel_page_table_range_lockless().
Reviewed-by: David Carlier <devnexen@gmail.com>
Cheers !
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-17 3:46 ` David CARLIER
@ 2026-07-17 7:47 ` Lorenzo Stoakes (ARM)
2026-07-17 8:05 ` David CARLIER
0 siblings, 1 reply; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-17 7:47 UTC (permalink / raw)
To: David CARLIER
Cc: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable
On Fri, Jul 17, 2026 at 04:46:42AM +0100, David CARLIER wrote:
> On Thu, 16 Jul 2026 at 22:31, Lorenzo Stoakes (ARM) <ljs@kernel.org> wrote:
> >
> > x86 implements page attribute modification using its Change Page
> > Attributes (CPA) mechanism.
> >
> > This tracks properties of ranges such as cache mode through x86 page
> > attributes, and as part of that logic manipulates kernel page tables.
> >
> > Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> > fragmentation") ranges of kernel page table entries can be collapsed into
> > huge page table entries as part of this logic.
> >
> > As part of this collapse, it frees the page tables which the collapsed
> > entries previously pointed to, and it does so without any relevant locks
> > being held to preclude concurrent kernel page table walkers.
> >
> > The only way this code can be reached is if CPA_COLLAPSE is specified, and
> > this is only set in set_memory_rox() via:
> >
> > set_memory_rox()
> > -> change_page_attr_set_clr()
> > -> cpa_flush()
> > -> cpa_collapse_large_pages()
> >
> > Notable users of this are execmem and bpf when manipulating executable
> > mappings.
> >
> > However, this is problematic for ptdump as it walks ranges it does not own
> > and thus runs the risk of a use-after-free on page tables freed underneath
> > it.
> >
> > In addition, concurrent CPA collapse operations are possible which can also
> > cause races.
> >
> > Resolve the issue by acquiring the mmap write lock on init_mm across the
> > whole operation.
> >
> > It is safe to acquire a sleeping lock as all the callers invoke
> > set_memory_rox() from process context and in any case,
> > change_page_attr_set_clr() calls vm_unmap_alias() which ultimately takes a
> > mutex, disallowing atomic context here.
> >
> > Fixes: 41d88484c71c ("x86/mm/pat: restore large ROX pages after fragmentation")
> > Cc: stable@vger.kernel.org
> > Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> > Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> > Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> > Reviewed-by: Dave Hansen <dave.hansen@linux.intel.com>
> > Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> > ---
> > arch/x86/mm/pat/set_memory.c | 15 ++++++++++++++-
> > include/linux/mmap_lock.h | 2 ++
> > 2 files changed, 16 insertions(+), 1 deletion(-)
> >
> > diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> > index d023a40a1e03..d1e63f7d267f 100644
> > --- a/arch/x86/mm/pat/set_memory.c
> > +++ b/arch/x86/mm/pat/set_memory.c
> > @@ -22,6 +22,7 @@
> > #include <linux/cc_platform.h>
> > #include <linux/set_memory.h>
> > #include <linux/memregion.h>
> > +#include <linux/cleanup.h>
> >
> > #include <asm/e820/api.h>
> > #include <asm/processor.h>
> > @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
> >
> > static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
> >
> > -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> > {
> > unsigned long start, addr, end;
> > struct ptdesc *ptdesc, *tmp;
> > @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > }
> > }
> >
> > +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > +{
> > + /*
> > + * Take the mmap write lock on init_mm to:
> > + * - Avoid a use-after-free if raced by ptdump (which takes its own
> > + * write lock on init_mm).
> > + * - Serialise concurrent CPA walkers.
> > + */
> > + scoped_guard(mmap_write_lock, &init_mm)
> > + __cpa_collapse_large_pages(cpa);
> > +}
> > +
> > static void cpa_flush(struct cpa_data *cpa, int cache)
> > {
> > unsigned int i;
> > diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
> > index 6b5c2390cc30..047f5f5e2c34 100644
> > --- a/include/linux/mmap_lock.h
> > +++ b/include/linux/mmap_lock.h
> > @@ -621,6 +621,8 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
> >
> > DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
> > mmap_read_lock(_T), mmap_read_unlock(_T))
> > +DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
> > + mmap_write_lock(_T), mmap_write_unlock(_T))
> > DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
> >
> > static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
> >
> > --
> > 2.55.0
> >
>
> Hi,
>
> This bullet confuses me. __change_page_attr() and friends are serialised by
Which bullet? You're quoting the whole mail?
Presumably "- Serialise concurrent CPA walkers".
> cpa_lock/pgd_lock and never take init_mm's mmap lock, so this doesn't
> exclude them.
>
> Collapse vs collapse looks covered already: collapse_large_pages() holds
cpa_collapse_large_pages() has 3 stages:
- walk + gather
- flush tlb
- free page tables
Holding a lock then releasing it across stages doesn't serialise the whole
operation.
Thus concurrent collapse can occur.
> pgd_lock across the walk and both collapse_pmd_page()/collapse_pud_page()
> calls, and the pmd_leaf()/pud_leaf() early-outs stop a second CPU
> re-queueing a collapsed table. I couldn't build the interleaving behind the
> cover letter's "could result in memory corruption". Can you spell it out?
"Build the interleaving" is an odd turn of phrase :)
>
> If the reason is really Will's v3 point - collapse rounds up to PMD/PUD and
> frees tables outside its caller's range, so it has to exclude
> walk_kernel_page_table_range() walkers holding the read lock - then say
> that. It's a UAF independent of ptdump, and it's why v3's read lock wasn't
> enough.
"Then say that" could be rephrased into something a little more polite :)
>
> Also: patch 1 justifies the trylock fallback on ptdump being rare, but this
> takes the write lock on every set_memory_rox() (BPF JIT, module load,
> execmem), even when nothing collapsed. vmap_try_huge_*() then loses the
Locks work by excluding the critical section.
> trylock and quietly falls back to PTEs on every BPF load. No counter, no
Entirely false. You don't have concurrent writers on every load.
> trace. Not a blocker and I've no better idea, but fa93b45fd397 was about
> getting vmalloc-huge on by default, so it's worth calling out.
I have no idea why you're referencing Dev's arm64 fix?
Anyway as to the 'costly' stuff - you're doing costly operations on this code
path anyway.
BPF JIT, module load + execmem allocation are not hot path operations nor do
they seem likely to be problematic in terms of lock contention.
>
> Rest looks right. 4/4 restores pud_free_pmd_page() exactly as it was before
> fa93b45fd397, and 3/4 builds the same walk init_mm got via
> walk_kernel_page_table_range_lockless().
??? I didn't change any of those in this revision? Let's keep review of a
specific patch to review of a specific patch, please :)
>
> Reviewed-by: David Carlier <devnexen@gmail.com>
Thanks.
>
> Cheers !
This reply reads like it was... assisted shall we say. Let me gently push back
on doing that please ;)
I don't think there are actually any other readers that could be problematic
here, so this a bit of a half-fix, really we need to do the same thing or take a
read lock on the split path too.
Let me think about it some more.
Thanks, Lorenzo
Assisted-by: Caffeine (Vimto energy with real fruit juice)
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-17 7:47 ` Lorenzo Stoakes (ARM)
@ 2026-07-17 8:05 ` David CARLIER
0 siblings, 0 replies; 12+ messages in thread
From: David CARLIER @ 2026-07-17 8:05 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable
Yes I used AI to agglomerate my otherwise disparate thoughts :)
On Fri, 17 Jul 2026 at 08:47, Lorenzo Stoakes (ARM) <ljs@kernel.org> wrote:
>
> On Fri, Jul 17, 2026 at 04:46:42AM +0100, David CARLIER wrote:
> > On Thu, 16 Jul 2026 at 22:31, Lorenzo Stoakes (ARM) <ljs@kernel.org> wrote:
> > >
> > > x86 implements page attribute modification using its Change Page
> > > Attributes (CPA) mechanism.
> > >
> > > This tracks properties of ranges such as cache mode through x86 page
> > > attributes, and as part of that logic manipulates kernel page tables.
> > >
> > > Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> > > fragmentation") ranges of kernel page table entries can be collapsed into
> > > huge page table entries as part of this logic.
> > >
> > > As part of this collapse, it frees the page tables which the collapsed
> > > entries previously pointed to, and it does so without any relevant locks
> > > being held to preclude concurrent kernel page table walkers.
> > >
> > > The only way this code can be reached is if CPA_COLLAPSE is specified, and
> > > this is only set in set_memory_rox() via:
> > >
> > > set_memory_rox()
> > > -> change_page_attr_set_clr()
> > > -> cpa_flush()
> > > -> cpa_collapse_large_pages()
> > >
> > > Notable users of this are execmem and bpf when manipulating executable
> > > mappings.
> > >
> > > However, this is problematic for ptdump as it walks ranges it does not own
> > > and thus runs the risk of a use-after-free on page tables freed underneath
> > > it.
> > >
> > > In addition, concurrent CPA collapse operations are possible which can also
> > > cause races.
> > >
> > > Resolve the issue by acquiring the mmap write lock on init_mm across the
> > > whole operation.
> > >
> > > It is safe to acquire a sleeping lock as all the callers invoke
> > > set_memory_rox() from process context and in any case,
> > > change_page_attr_set_clr() calls vm_unmap_alias() which ultimately takes a
> > > mutex, disallowing atomic context here.
> > >
> > > Fixes: 41d88484c71c ("x86/mm/pat: restore large ROX pages after fragmentation")
> > > Cc: stable@vger.kernel.org
> > > Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> > > Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> > > Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> > > Reviewed-by: Dave Hansen <dave.hansen@linux.intel.com>
> > > Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> > > ---
> > > arch/x86/mm/pat/set_memory.c | 15 ++++++++++++++-
> > > include/linux/mmap_lock.h | 2 ++
> > > 2 files changed, 16 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> > > index d023a40a1e03..d1e63f7d267f 100644
> > > --- a/arch/x86/mm/pat/set_memory.c
> > > +++ b/arch/x86/mm/pat/set_memory.c
> > > @@ -22,6 +22,7 @@
> > > #include <linux/cc_platform.h>
> > > #include <linux/set_memory.h>
> > > #include <linux/memregion.h>
> > > +#include <linux/cleanup.h>
> > >
> > > #include <asm/e820/api.h>
> > > #include <asm/processor.h>
> > > @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
> > >
> > > static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
> > >
> > > -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > > +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> > > {
> > > unsigned long start, addr, end;
> > > struct ptdesc *ptdesc, *tmp;
> > > @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > > }
> > > }
> > >
> > > +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > > +{
> > > + /*
> > > + * Take the mmap write lock on init_mm to:
> > > + * - Avoid a use-after-free if raced by ptdump (which takes its own
> > > + * write lock on init_mm).
> > > + * - Serialise concurrent CPA walkers.
> > > + */
> > > + scoped_guard(mmap_write_lock, &init_mm)
> > > + __cpa_collapse_large_pages(cpa);
> > > +}
> > > +
> > > static void cpa_flush(struct cpa_data *cpa, int cache)
> > > {
> > > unsigned int i;
> > > diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
> > > index 6b5c2390cc30..047f5f5e2c34 100644
> > > --- a/include/linux/mmap_lock.h
> > > +++ b/include/linux/mmap_lock.h
> > > @@ -621,6 +621,8 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
> > >
> > > DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
> > > mmap_read_lock(_T), mmap_read_unlock(_T))
> > > +DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
> > > + mmap_write_lock(_T), mmap_write_unlock(_T))
> > > DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
> > >
> > > static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
> > >
> > > --
> > > 2.55.0
> > >
> >
> > Hi,
> >
> > This bullet confuses me. __change_page_attr() and friends are serialised by
>
> Which bullet? You're quoting the whole mail?
>
> Presumably "- Serialise concurrent CPA walkers".
>
> > cpa_lock/pgd_lock and never take init_mm's mmap lock, so this doesn't
> > exclude them.
> >
> > Collapse vs collapse looks covered already: collapse_large_pages() holds
>
> cpa_collapse_large_pages() has 3 stages:
>
> - walk + gather
> - flush tlb
> - free page tables
>
> Holding a lock then releasing it across stages doesn't serialise the whole
> operation.
>
> Thus concurrent collapse can occur.
>
> > pgd_lock across the walk and both collapse_pmd_page()/collapse_pud_page()
> > calls, and the pmd_leaf()/pud_leaf() early-outs stop a second CPU
> > re-queueing a collapsed table. I couldn't build the interleaving behind the
> > cover letter's "could result in memory corruption". Can you spell it out?
>
> "Build the interleaving" is an odd turn of phrase :)
>
> >
> > If the reason is really Will's v3 point - collapse rounds up to PMD/PUD and
> > frees tables outside its caller's range, so it has to exclude
> > walk_kernel_page_table_range() walkers holding the read lock - then say
> > that. It's a UAF independent of ptdump, and it's why v3's read lock wasn't
> > enough.
>
> "Then say that" could be rephrased into something a little more polite :)
>
Sorry I did not mean that tone originally
> >
> > Also: patch 1 justifies the trylock fallback on ptdump being rare, but this
> > takes the write lock on every set_memory_rox() (BPF JIT, module load,
> > execmem), even when nothing collapsed. vmap_try_huge_*() then loses the
>
> Locks work by excluding the critical section.
>
> > trylock and quietly falls back to PTEs on every BPF load. No counter, no
>
> Entirely false. You don't have concurrent writers on every load.
>
> > trace. Not a blocker and I've no better idea, but fa93b45fd397 was about
> > getting vmalloc-huge on by default, so it's worth calling out.
>
> I have no idea why you're referencing Dev's arm64 fix?
>
> Anyway as to the 'costly' stuff - you're doing costly operations on this code
> path anyway.
>
> BPF JIT, module load + execmem allocation are not hot path operations nor do
> they seem likely to be problematic in terms of lock contention.
>
> >
> > Rest looks right. 4/4 restores pud_free_pmd_page() exactly as it was before
> > fa93b45fd397, and 3/4 builds the same walk init_mm got via
> > walk_kernel_page_table_range_lockless().
>
> ??? I didn't change any of those in this revision? Let's keep review of a
> specific patch to review of a specific patch, please :)
>
> >
> > Reviewed-by: David Carlier <devnexen@gmail.com>
>
> Thanks.
>
> >
> > Cheers !
>
> This reply reads like it was... assisted shall we say. Let me gently push back
> on doing that please ;)
>
> I don't think there are actually any other readers that could be problematic
> here, so this a bit of a half-fix, really we need to do the same thing or take a
> read lock on the split path too.
>
> Let me think about it some more.
>
> Thanks, Lorenzo
>
> Assisted-by: Caffeine (Vimto energy with real fruit juice)
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF Lorenzo Stoakes (ARM)
2026-07-17 3:46 ` David CARLIER
@ 2026-07-17 11:30 ` Will Deacon
2026-07-17 11:45 ` Lorenzo Stoakes (ARM)
2026-07-17 11:40 ` Lorenzo Stoakes (ARM)
2 siblings, 1 reply; 12+ messages in thread
From: Will Deacon @ 2026-07-17 11:30 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Dev Jain, Ryan Roberts, David Carlier, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable
On Thu, Jul 16, 2026 at 10:31:13PM +0100, Lorenzo Stoakes (ARM) wrote:
> x86 implements page attribute modification using its Change Page
> Attributes (CPA) mechanism.
>
> This tracks properties of ranges such as cache mode through x86 page
> attributes, and as part of that logic manipulates kernel page tables.
>
> Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> fragmentation") ranges of kernel page table entries can be collapsed into
> huge page table entries as part of this logic.
>
> As part of this collapse, it frees the page tables which the collapsed
> entries previously pointed to, and it does so without any relevant locks
> being held to preclude concurrent kernel page table walkers.
>
> The only way this code can be reached is if CPA_COLLAPSE is specified, and
> this is only set in set_memory_rox() via:
>
> set_memory_rox()
> -> change_page_attr_set_clr()
> -> cpa_flush()
> -> cpa_collapse_large_pages()
>
> Notable users of this are execmem and bpf when manipulating executable
> mappings.
>
> However, this is problematic for ptdump as it walks ranges it does not own
> and thus runs the risk of a use-after-free on page tables freed underneath
> it.
>
> In addition, concurrent CPA collapse operations are possible which can also
> cause races.
>
> Resolve the issue by acquiring the mmap write lock on init_mm across the
> whole operation.
Thanks, Lorenzo.
This ain't worth much here, but all the same:
Reviewed-by: Will Deacon <will@kernel.org>
with one nit below:
> diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> index d023a40a1e03..d1e63f7d267f 100644
> --- a/arch/x86/mm/pat/set_memory.c
> +++ b/arch/x86/mm/pat/set_memory.c
> @@ -22,6 +22,7 @@
> #include <linux/cc_platform.h>
> #include <linux/set_memory.h>
> #include <linux/memregion.h>
> +#include <linux/cleanup.h>
>
> #include <asm/e820/api.h>
> #include <asm/processor.h>
> @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
>
> static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
>
> -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> {
> unsigned long start, addr, end;
> struct ptdesc *ptdesc, *tmp;
> @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> }
> }
>
> +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +{
> + /*
> + * Take the mmap write lock on init_mm to:
> + * - Avoid a use-after-free if raced by ptdump (which takes its own
> + * write lock on init_mm).
> + * - Serialise concurrent CPA walkers.
The CPA lock should handle those, so I think it's the non-CPA walkers
that are problematic. Looks like Sashiko reckons it's found some that
don't take the mmap_lock at all [1], but this is still a big step in the
right direction, fixes the ptdump case and we're not trying to boil the
ocean.
Will
[1] https://lore.kernel.org/all/20260716215307.14A6E1F000E9@smtp.kernel.org/#t
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-17 11:30 ` Will Deacon
@ 2026-07-17 11:45 ` Lorenzo Stoakes (ARM)
0 siblings, 0 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-17 11:45 UTC (permalink / raw)
To: Will Deacon
Cc: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Dev Jain, Ryan Roberts, David Carlier, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable
On Fri, Jul 17, 2026 at 12:30:38PM +0100, Will Deacon wrote:
> On Thu, Jul 16, 2026 at 10:31:13PM +0100, Lorenzo Stoakes (ARM) wrote:
> > x86 implements page attribute modification using its Change Page
> > Attributes (CPA) mechanism.
> >
> > This tracks properties of ranges such as cache mode through x86 page
> > attributes, and as part of that logic manipulates kernel page tables.
> >
> > Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> > fragmentation") ranges of kernel page table entries can be collapsed into
> > huge page table entries as part of this logic.
> >
> > As part of this collapse, it frees the page tables which the collapsed
> > entries previously pointed to, and it does so without any relevant locks
> > being held to preclude concurrent kernel page table walkers.
> >
> > The only way this code can be reached is if CPA_COLLAPSE is specified, and
> > this is only set in set_memory_rox() via:
> >
> > set_memory_rox()
> > -> change_page_attr_set_clr()
> > -> cpa_flush()
> > -> cpa_collapse_large_pages()
> >
> > Notable users of this are execmem and bpf when manipulating executable
> > mappings.
> >
> > However, this is problematic for ptdump as it walks ranges it does not own
> > and thus runs the risk of a use-after-free on page tables freed underneath
> > it.
> >
> > In addition, concurrent CPA collapse operations are possible which can also
> > cause races.
> >
> > Resolve the issue by acquiring the mmap write lock on init_mm across the
> > whole operation.
>
> Thanks, Lorenzo.
>
> This ain't worth much here, but all the same:
>
> Reviewed-by: Will Deacon <will@kernel.org>
Always appreciated :>) thanks!
>
> with one nit below:
>
> > diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> > index d023a40a1e03..d1e63f7d267f 100644
> > --- a/arch/x86/mm/pat/set_memory.c
> > +++ b/arch/x86/mm/pat/set_memory.c
> > @@ -22,6 +22,7 @@
> > #include <linux/cc_platform.h>
> > #include <linux/set_memory.h>
> > #include <linux/memregion.h>
> > +#include <linux/cleanup.h>
> >
> > #include <asm/e820/api.h>
> > #include <asm/processor.h>
> > @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
> >
> > static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
> >
> > -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> > {
> > unsigned long start, addr, end;
> > struct ptdesc *ptdesc, *tmp;
> > @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > }
> > }
> >
> > +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> > +{
> > + /*
> > + * Take the mmap write lock on init_mm to:
> > + * - Avoid a use-after-free if raced by ptdump (which takes its own
> > + * write lock on init_mm).
> > + * - Serialise concurrent CPA walkers.
>
> The CPA lock should handle those, so I think it's the non-CPA walkers
Ah well no actually, see Denis's patch at [2] - we actually have a race here
that this doesn't quite solve.
But. See my reply at [3] - if people are agreeable to that I'll respin with an
extra patch to fix that which this change now permits :))
> that are problematic. Looks like Sashiko reckons it's found some that
> don't take the mmap_lock at all [1], but this is still a big step in the
> right direction, fixes the ptdump case and we're not trying to boil the
> ocean.
>
> Will
>
> [1] https://lore.kernel.org/all/20260716215307.14A6E1F000E9@smtp.kernel.org/#t
In the long run I'm looking into RCU-safe page table freeing more generally, but
obviously here we're trying to get things to a state of vague sanity (and
probably we're not getting _all_ the races anyway) first.
Cheers, Lorenzo
[2]:https://lore.kernel.org/all/20260626163213.2284080-1-den@openvz.org/
[3]:https://lore.kernel.org/all/20260716-series-vmap-race-fix-v4-0-8c108c4317df@kernel.org/
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF Lorenzo Stoakes (ARM)
2026-07-17 3:46 ` David CARLIER
2026-07-17 11:30 ` Will Deacon
@ 2026-07-17 11:40 ` Lorenzo Stoakes (ARM)
2 siblings, 0 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-17 11:40 UTC (permalink / raw)
To: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts
Cc: David Carlier, linux-mm, linux-kernel, bpf, linux-arm-kernel,
Denis V. Lunev, stable
On Thu, Jul 16, 2026 at 10:31:13PM +0100, Lorenzo Stoakes (ARM) wrote:
OK so note to self - don't send series late in the evening when you're
exhausted :)
I rather glossed over the problem, Will's reply, and Denis's excellent bug
report at [0].
So. The (other) problem with CPA, as Denis pointed out, is that
__change_page_attr() can update something in a page table that is
concurrently being torn down, with set_pte_atomic() being the point at
which the UAF occurs.
With the switch to a write lock we can simply do:
scoped_guard(mmap_read_lock, &init_mm)
ret = __change_page_attr_set_clr(&cpa, 1);
In change_page_attr_set_clr().
Since CPA_COLLAPSE is only ever invoked via set_memory_rox() collapse is
only done via change_page_attr_set_clr() and this covers all callers which
are impacted by this.
If people are agreeable to this I will respin with an additional patch to
do this, as it makes sense to bundle it in this series with its depend
change (this patch) included and properly ordered.
Cheers, Lorenzo
[0]:https://lore.kernel.org/all/20260626163213.2284080-1-den@openvz.org/
> x86 implements page attribute modification using its Change Page
> Attributes (CPA) mechanism.
>
> This tracks properties of ranges such as cache mode through x86 page
> attributes, and as part of that logic manipulates kernel page tables.
>
> Since commit 41d88484c71c ("x86/mm/pat: restore large ROX pages after
> fragmentation") ranges of kernel page table entries can be collapsed into
> huge page table entries as part of this logic.
>
> As part of this collapse, it frees the page tables which the collapsed
> entries previously pointed to, and it does so without any relevant locks
> being held to preclude concurrent kernel page table walkers.
>
> The only way this code can be reached is if CPA_COLLAPSE is specified, and
> this is only set in set_memory_rox() via:
>
> set_memory_rox()
> -> change_page_attr_set_clr()
> -> cpa_flush()
> -> cpa_collapse_large_pages()
>
> Notable users of this are execmem and bpf when manipulating executable
> mappings.
>
> However, this is problematic for ptdump as it walks ranges it does not own
> and thus runs the risk of a use-after-free on page tables freed underneath
> it.
>
> In addition, concurrent CPA collapse operations are possible which can also
> cause races.
>
> Resolve the issue by acquiring the mmap write lock on init_mm across the
> whole operation.
>
> It is safe to acquire a sleeping lock as all the callers invoke
> set_memory_rox() from process context and in any case,
> change_page_attr_set_clr() calls vm_unmap_alias() which ultimately takes a
> mutex, disallowing atomic context here.
>
> Fixes: 41d88484c71c ("x86/mm/pat: restore large ROX pages after fragmentation")
> Cc: stable@vger.kernel.org
> Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
> Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
> Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> Reviewed-by: Dave Hansen <dave.hansen@linux.intel.com>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> ---
> arch/x86/mm/pat/set_memory.c | 15 ++++++++++++++-
> include/linux/mmap_lock.h | 2 ++
> 2 files changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/arch/x86/mm/pat/set_memory.c b/arch/x86/mm/pat/set_memory.c
> index d023a40a1e03..d1e63f7d267f 100644
> --- a/arch/x86/mm/pat/set_memory.c
> +++ b/arch/x86/mm/pat/set_memory.c
> @@ -22,6 +22,7 @@
> #include <linux/cc_platform.h>
> #include <linux/set_memory.h>
> #include <linux/memregion.h>
> +#include <linux/cleanup.h>
>
> #include <asm/e820/api.h>
> #include <asm/processor.h>
> @@ -410,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
>
> static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
>
> -static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +static void __cpa_collapse_large_pages(struct cpa_data *cpa)
> {
> unsigned long start, addr, end;
> struct ptdesc *ptdesc, *tmp;
> @@ -442,6 +443,18 @@ static void cpa_collapse_large_pages(struct cpa_data *cpa)
> }
> }
>
> +static void cpa_collapse_large_pages(struct cpa_data *cpa)
> +{
> + /*
> + * Take the mmap write lock on init_mm to:
> + * - Avoid a use-after-free if raced by ptdump (which takes its own
> + * write lock on init_mm).
> + * - Serialise concurrent CPA walkers.
> + */
> + scoped_guard(mmap_write_lock, &init_mm)
> + __cpa_collapse_large_pages(cpa);
> +}
> +
> static void cpa_flush(struct cpa_data *cpa, int cache)
> {
> unsigned int i;
> diff --git a/include/linux/mmap_lock.h b/include/linux/mmap_lock.h
> index 6b5c2390cc30..047f5f5e2c34 100644
> --- a/include/linux/mmap_lock.h
> +++ b/include/linux/mmap_lock.h
> @@ -621,6 +621,8 @@ static inline void mmap_read_unlock(struct mm_struct *mm)
>
> DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
> mmap_read_lock(_T), mmap_read_unlock(_T))
> +DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
> + mmap_write_lock(_T), mmap_write_unlock(_T))
> DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
>
> static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
>
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH mm-hotfixes v4 3/4] mm/ptdump: always stabilise against page table freeing using init_mm
2026-07-16 21:31 [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Lorenzo Stoakes (ARM)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 1/4] mm/vmalloc: acquire init_mm lock on huge vmap to avoid ptdump UAF Lorenzo Stoakes (ARM)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 2/4] x86/mm/pat: acquire init_mm write lock to avoid UAF Lorenzo Stoakes (ARM)
@ 2026-07-16 21:31 ` Lorenzo Stoakes (ARM)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 4/4] arm64: remove redundant concurrent ptdump UAF mitigation Lorenzo Stoakes (ARM)
2026-07-16 23:41 ` [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Andrew Morton
4 siblings, 0 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-16 21:31 UTC (permalink / raw)
To: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts
Cc: David Carlier, ljs, linux-mm, linux-kernel, bpf, linux-arm-kernel,
Denis V. Lunev, stable
Previous commits have established the invariant that kernel page table
freeing is performed while an mmap read lock on init_mm is held, which
fixes races between ptdump and kernel page table freeing over init_mm.
However, x86 and arm64 can perform a ptdump over an mm other than init_mm
via ptdump_walk_pgd() and since kernel memory ranges are shared across
non-kernel mm's, this means that the race still exists for these cases.
Fix this by acquiring a nested mmap write lock for init_mm in
ptdump_walk_pgd().
This is safe as we take this after mmap write locking the mm, and nothing
acquires the init_mm lock first before locking an arbitrary mm, so no
deadlock is possible.
Also update walk_page_range_debug() to assert that init_mm is write locked,
add a comment explaining why and remove some redundant code, and eliminate
the unnecessary and confusing invocation of walk_kernel_page_table_range().
We can safely remove the non-NULL check for walk.mm, as the mmap lock
asserts would NULL pointer deref if it was (and of course no callers do
this).
The first point at which ptdump can race kernel page table freeing is
commit b6bdb7517c3d ("mm/vmalloc: add interfaces to free unmapped page
table"), so we target this in the Fixes tag.
Fixes: b6bdb7517c3d ("mm/vmalloc: add interfaces to free unmapped page table")
Cc: stable@vger.kernel.org
Reviewed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Acked-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
mm/pagewalk.c | 14 +++++++++-----
mm/ptdump.c | 7 +++++++
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/mm/pagewalk.c b/mm/pagewalk.c
index bbcfd68d0907..5d87c632a255 100644
--- a/mm/pagewalk.c
+++ b/mm/pagewalk.c
@@ -702,12 +702,16 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start,
* to account for page table freeing on vmap huge page mapping.
*/
mmap_assert_write_locked(mm);
+ /*
+ * x86, arm64 ptdump allow walks of efi mm's and x86 ptdump allows walks
+ * of arbitrary mm's.
+ *
+ * However, they both must also hold the init_mm lock to account for
+ * concurrent kernel page table freeing.
+ */
+ mmap_assert_write_locked(&init_mm);
- /* For convenience, we allow traversal of kernel mappings. */
- if (mm == &init_mm)
- return walk_kernel_page_table_range(start, end, ops,
- pgd, private);
- if (start >= end || !walk.mm)
+ if (start >= end)
return -EINVAL;
if (!check_ops_safe(ops))
return -EINVAL;
diff --git a/mm/ptdump.c b/mm/ptdump.c
index 973020000096..5851096e6f65 100644
--- a/mm/ptdump.c
+++ b/mm/ptdump.c
@@ -178,11 +178,18 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)
get_online_mems();
mmap_write_lock(mm);
+ /* To stabilise kernel page tables we must hold the init_mm lock too. */
+ if (mm != &init_mm)
+ mmap_write_lock_nested(&init_mm, SINGLE_DEPTH_NESTING);
+
while (range->start != range->end) {
walk_page_range_debug(mm, range->start, range->end,
&ptdump_ops, pgd, st);
range++;
}
+
+ if (mm != &init_mm)
+ mmap_write_unlock(&init_mm);
mmap_write_unlock(mm);
put_online_mems();
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH mm-hotfixes v4 4/4] arm64: remove redundant concurrent ptdump UAF mitigation
2026-07-16 21:31 [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Lorenzo Stoakes (ARM)
` (2 preceding siblings ...)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 3/4] mm/ptdump: always stabilise against page table freeing using init_mm Lorenzo Stoakes (ARM)
@ 2026-07-16 21:31 ` Lorenzo Stoakes (ARM)
2026-07-16 23:41 ` [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Andrew Morton
4 siblings, 0 replies; 12+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-16 21:31 UTC (permalink / raw)
To: Andrew Morton, Suren Baghdasaryan, Liam R. Howlett,
Vlastimil Babka, Shakeel Butt, David Hildenbrand, Mike Rapoport,
Michal Hocko, Uladzislau Rezki, Toshi Kani, Dave Hansen,
Andy Lutomirski, Peter Zijlstra, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, x86, H. Peter Anvin, Kiryl Shutsemau,
Catalin Marinas, Will Deacon, Dev Jain, Ryan Roberts
Cc: David Carlier, ljs, linux-mm, linux-kernel, bpf, linux-arm-kernel,
Denis V. Lunev, stable
This partially reverts commit fa93b45fd397 ("arm64: Enable vmalloc-huge
with ptdump"), retaining vmalloc-huge support but eliminating the now
redundant mitigation against a race between huge vmap page table freeing
and ptdump, as this issue has now been fixed at core.
We also simultaneously remove the arm64 if-deffery when acquiring the mmap
read lock upon vmap huge page table promotion as it is no longer required.
Note that this patch relies on the preceding vmalloc patch, and should not
be backported alone.
Fixes: fa93b45fd397 ("arm64: Enable vmalloc-huge with ptdump")
Cc: stable@vger.kernel.org
Reviewed-by: Dev Jain <dev.jain@arm.com>
Acked-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Acked-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Acked-by: Will Deacon <will@kernel.org>
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
arch/arm64/include/asm/ptdump.h | 2 --
arch/arm64/mm/mmu.c | 43 ++++-------------------------------------
arch/arm64/mm/ptdump.c | 11 ++---------
mm/vmalloc.c | 15 +++-----------
4 files changed, 9 insertions(+), 62 deletions(-)
diff --git a/arch/arm64/include/asm/ptdump.h b/arch/arm64/include/asm/ptdump.h
index 5b374a6ab34a..50a195eda8ed 100644
--- a/arch/arm64/include/asm/ptdump.h
+++ b/arch/arm64/include/asm/ptdump.h
@@ -7,8 +7,6 @@
#include <linux/ptdump.h>
-DECLARE_STATIC_KEY_FALSE(arm64_ptdump_lock_key);
-
#ifdef CONFIG_PTDUMP
#include <linux/mm_types.h>
diff --git a/arch/arm64/mm/mmu.c b/arch/arm64/mm/mmu.c
index a25d8beacc83..bd52fca6e872 100644
--- a/arch/arm64/mm/mmu.c
+++ b/arch/arm64/mm/mmu.c
@@ -49,8 +49,6 @@
#define NO_CONT_MAPPINGS BIT(1)
#define NO_EXEC_MAPPINGS BIT(2) /* assumes FEAT_HPDS is not used */
-DEFINE_STATIC_KEY_FALSE(arm64_ptdump_lock_key);
-
u64 kimage_voffset __ro_after_init;
EXPORT_SYMBOL(kimage_voffset);
@@ -1864,8 +1862,7 @@ int pmd_clear_huge(pmd_t *pmdp)
return 1;
}
-static int __pmd_free_pte_page(pmd_t *pmdp, unsigned long addr,
- bool acquire_mmap_lock)
+int pmd_free_pte_page(pmd_t *pmdp, unsigned long addr)
{
pte_t *table;
pmd_t pmd;
@@ -1877,25 +1874,13 @@ static int __pmd_free_pte_page(pmd_t *pmdp, unsigned long addr,
return 1;
}
- /* See comment in pud_free_pmd_page for static key logic */
table = pte_offset_kernel(pmdp, addr);
pmd_clear(pmdp);
__flush_tlb_kernel_pgtable(addr);
- if (static_branch_unlikely(&arm64_ptdump_lock_key) && acquire_mmap_lock) {
- mmap_read_lock(&init_mm);
- mmap_read_unlock(&init_mm);
- }
-
pte_free_kernel(NULL, table);
return 1;
}
-int pmd_free_pte_page(pmd_t *pmdp, unsigned long addr)
-{
- /* If ptdump is walking the pagetables, acquire init_mm.mmap_lock */
- return __pmd_free_pte_page(pmdp, addr, /* acquire_mmap_lock = */ true);
-}
-
int pud_free_pmd_page(pud_t *pudp, unsigned long addr)
{
pmd_t *table;
@@ -1911,36 +1896,16 @@ int pud_free_pmd_page(pud_t *pudp, unsigned long addr)
}
table = pmd_offset(pudp, addr);
-
- /*
- * Our objective is to prevent ptdump from reading a PMD table which has
- * been freed. In this race, if pud_free_pmd_page observes the key on
- * (which got flipped by ptdump) then the mmap lock sequence here will,
- * as a result of the mmap write lock/unlock sequence in ptdump, give
- * us the correct synchronization. If not, this means that ptdump has
- * yet not started walking the pagetables - the sequence of barriers
- * issued by __flush_tlb_kernel_pgtable() guarantees that ptdump will
- * observe an empty PUD.
- */
- pud_clear(pudp);
- __flush_tlb_kernel_pgtable(addr);
- if (static_branch_unlikely(&arm64_ptdump_lock_key)) {
- mmap_read_lock(&init_mm);
- mmap_read_unlock(&init_mm);
- }
-
pmdp = table;
next = addr;
end = addr + PUD_SIZE;
do {
if (pmd_present(pmdp_get(pmdp)))
- /*
- * PMD has been isolated, so ptdump won't see it. No
- * need to acquire init_mm.mmap_lock.
- */
- __pmd_free_pte_page(pmdp, next, /* acquire_mmap_lock = */ false);
+ pmd_free_pte_page(pmdp, next);
} while (pmdp++, next += PMD_SIZE, next != end);
+ pud_clear(pudp);
+ __flush_tlb_kernel_pgtable(addr);
pmd_free(NULL, table);
return 1;
}
diff --git a/arch/arm64/mm/ptdump.c b/arch/arm64/mm/ptdump.c
index 1c20144700d7..5a76c59b5ada 100644
--- a/arch/arm64/mm/ptdump.c
+++ b/arch/arm64/mm/ptdump.c
@@ -283,13 +283,6 @@ void note_page_flush(struct ptdump_state *pt_st)
note_page(pt_st, 0, -1, pte_val(pte_zero));
}
-static void arm64_ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm)
-{
- static_branch_inc(&arm64_ptdump_lock_key);
- ptdump_walk_pgd(st, mm, NULL);
- static_branch_dec(&arm64_ptdump_lock_key);
-}
-
void ptdump_walk(struct seq_file *s, struct ptdump_info *info)
{
unsigned long end = ~0UL;
@@ -318,7 +311,7 @@ void ptdump_walk(struct seq_file *s, struct ptdump_info *info)
}
};
- arm64_ptdump_walk_pgd(&st.ptdump, info->mm);
+ ptdump_walk_pgd(&st.ptdump, info->mm, NULL);
}
static void __init ptdump_initialize(void)
@@ -360,7 +353,7 @@ bool ptdump_check_wx(void)
}
};
- arm64_ptdump_walk_pgd(&st.ptdump, &init_mm);
+ ptdump_walk_pgd(&st.ptdump, &init_mm, NULL);
if (st.wx_pages || st.uxn_pages) {
pr_warn("Checked W+X mappings: FAILED, %lu W+X pages found, %lu non-UXN pages found\n",
diff --git a/mm/vmalloc.c b/mm/vmalloc.c
index d5c4d2bb770b..f4fa227a8d7f 100644
--- a/mm/vmalloc.c
+++ b/mm/vmalloc.c
@@ -169,10 +169,7 @@ static int vmap_try_huge_pmd(pmd_t *pmd, unsigned long addr, unsigned long end,
* Concurrent read lock holders are safe: each exclusively owns
* the range it operates on and cannot reach this page table.
*/
-#ifndef CONFIG_ARM64
- scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
-#endif
- {
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm) {
if (!pmd_free_pte_page(pmd, addr))
return 0;
return pmd_set_huge(pmd, phys_addr, prot);
@@ -229,10 +226,7 @@ static int vmap_try_huge_pud(pud_t *pud, unsigned long addr, unsigned long end,
return pud_set_huge(pud, phys_addr, prot);
/* See comment in vmap_try_huge_pmd(). */
-#ifndef CONFIG_ARM64
- scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
-#endif
- {
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm) {
if (!pud_free_pmd_page(pud, addr))
return 0;
return pud_set_huge(pud, phys_addr, prot);
@@ -289,10 +283,7 @@ static int vmap_try_huge_p4d(p4d_t *p4d, unsigned long addr, unsigned long end,
return p4d_set_huge(p4d, phys_addr, prot);
/* See comment in vmap_try_huge_pmd(). */
-#ifndef CONFIG_ARM64
- scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm)
-#endif
- {
+ scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm) {
if (!p4d_free_pud_page(p4d, addr))
return 0;
return p4d_set_huge(p4d, phys_addr, prot);
--
2.55.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing
2026-07-16 21:31 [PATCH mm-hotfixes v4 0/4] mm: fix UAF caused by race between ptdump and vmap pgtable freeing Lorenzo Stoakes (ARM)
` (3 preceding siblings ...)
2026-07-16 21:31 ` [PATCH mm-hotfixes v4 4/4] arm64: remove redundant concurrent ptdump UAF mitigation Lorenzo Stoakes (ARM)
@ 2026-07-16 23:41 ` Andrew Morton
4 siblings, 0 replies; 12+ messages in thread
From: Andrew Morton @ 2026-07-16 23:41 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Suren Baghdasaryan, Liam R. Howlett, Vlastimil Babka,
Shakeel Butt, David Hildenbrand, Mike Rapoport, Michal Hocko,
Uladzislau Rezki, Toshi Kani, Dave Hansen, Andy Lutomirski,
Peter Zijlstra, Thomas Gleixner, Ingo Molnar, Borislav Petkov,
x86, H. Peter Anvin, Kiryl Shutsemau, Catalin Marinas,
Will Deacon, Dev Jain, Ryan Roberts, David Carlier, linux-mm,
linux-kernel, bpf, linux-arm-kernel, Denis V. Lunev, stable,
syzbot+fd95a72470f5a44e464c
On Thu, 16 Jul 2026 22:31:11 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:
> Kernel page table walkers fall into two broad categories - those ranges
> where no exclusion is required via walk_kernel_page_table_range_lockless()
> and those where exclusion is required via walk_kernel_page_table_range()
> or walk_page_range_debug().
>
> ...
>
> This series works around this by #ifndef CONFIG_ARM64'ing the mmap read
> lock in vmap logic, then partially reverting commit fa93b45fd397 ("arm64:
> Enable vmalloc-huge with ptdump"), keeping the enablement of huge vmap
> support, and removing the ifdeffery with the partial revert patch.
Thanks, I've updated mm.git's mm-hotfixes-unstable branch to this
version.
> v4:
> * Rebased on latest master of Linus's tree.
> * Accumulated tags, thanks everybody!
> * Updated commit messages further as per Kiryl.
> * Took the mmap write lock across the whole CPA operation as per Will.
Here's how v4 altered mm.git:
arch/x86/mm/pat/set_memory.c | 25 +++++++++++++++----------
include/linux/mmap_lock.h | 2 ++
mm/vmalloc.c | 8 ++++----
3 files changed, 21 insertions(+), 14 deletions(-)
--- a/arch/x86/mm/pat/set_memory.c~b
+++ a/arch/x86/mm/pat/set_memory.c
@@ -411,7 +411,7 @@ static void __cpa_flush_tlb(void *data)
static int collapse_large_pages(unsigned long addr, struct list_head *pgtables);
-static void cpa_collapse_large_pages(struct cpa_data *cpa)
+static void __cpa_collapse_large_pages(struct cpa_data *cpa)
{
unsigned long start, addr, end;
struct ptdesc *ptdesc, *tmp;
@@ -437,17 +437,22 @@ static void cpa_collapse_large_pages(str
flush_tlb_all();
+ list_for_each_entry_safe(ptdesc, tmp, &pgtables, pt_list) {
+ list_del(&ptdesc->pt_list);
+ pagetable_free(ptdesc);
+ }
+}
+
+static void cpa_collapse_large_pages(struct cpa_data *cpa)
+{
/*
- * ptdump might read these page tables, so avoid a use-after-free by
- * acquiring the mmap read lock on init_mm (ptdump acquires the mmap
- * write lock).
+ * Take the mmap write lock on init_mm to:
+ * - Avoid a use-after-free if raced by ptdump (which takes its own
+ * write lock on init_mm).
+ * - Serialise concurrent CPA walkers.
*/
- scoped_guard(mmap_read_lock, &init_mm) {
- list_for_each_entry_safe(ptdesc, tmp, &pgtables, pt_list) {
- list_del(&ptdesc->pt_list);
- pagetable_free(ptdesc);
- }
- }
+ scoped_guard(mmap_write_lock, &init_mm)
+ __cpa_collapse_large_pages(cpa);
}
static void cpa_flush(struct cpa_data *cpa, int cache)
--- a/include/linux/mmap_lock.h~b
+++ a/include/linux/mmap_lock.h
@@ -621,6 +621,8 @@ static inline void mmap_read_unlock(stru
DEFINE_GUARD(mmap_read_lock, struct mm_struct *,
mmap_read_lock(_T), mmap_read_unlock(_T))
+DEFINE_GUARD(mmap_write_lock, struct mm_struct *,
+ mmap_write_lock(_T), mmap_write_unlock(_T))
DEFINE_GUARD_COND(mmap_read_lock, _try, mmap_read_trylock(_T))
static inline void mmap_read_unlock_non_owner(struct mm_struct *mm)
--- a/mm/vmalloc.c~b
+++ a/mm/vmalloc.c
@@ -163,11 +163,11 @@ static int vmap_try_huge_pmd(pmd_t *pmd,
return pmd_set_huge(pmd, phys_addr, prot);
/*
- * Acquire the mmap read lock to exclude ptdump, which walks kernel
- * page tables it does not own under the mmap write lock.
+ * Acquire the mmap read lock to exclude ptdump, which walks
+ * kernel page tables it does not own under the mmap write lock.
*
- * Concurrent read lock holders are safe: each exclusively owns the
- * range it operates on and cannot reach this page table.
+ * Concurrent read lock holders are safe: each exclusively owns
+ * the range it operates on and cannot reach this page table.
*/
scoped_cond_guard(mmap_read_lock_try, return 0, &init_mm) {
if (!pmd_free_pte_page(pmd, addr))
_
^ permalink raw reply [flat|nested] 12+ messages in thread