mm: pgtable: free kernel page tables via RCU to fix ptdump UAF
Subsystems (3)
PATCH 1/1 mm: pgtable: free kernel page tables via RCU to fix ptdump UAF
ptdump_walk_pgd() walks the kernel page tables under get_online_mems() and mmap_write_lock(&init_mm). Neither lock stops vmalloc from freeing a kernel PTE page underneath the walk.
When vmap_try_huge_pmd() promotes a range to a huge PMD it collapses the existing PTE table and frees it via pmd_free_pte_page(). On x86, riscv and powerpc this runs without the init_mm mmap lock; only arm64 takes it, and not on the block-split path. So ptdump can dereference a just-freed PTE page, which is the use after free syzbot hit in ptdump_pte_entry().
The race is not new. ptdump walks the whole kernel address space, including ranges other code is actively mapping, so it reads page tables it does not own. 5ba2f0a15564 ("mm: introduce deferred freeing for kernel page tables") only widened the window; the Fixes tag points there for that reason.
Every other walker works on a range it owns and is the only one mutating it:set_memory() on arm64/riscv/loongarch, the arm64 block-split path, the openrisc DMA path and the hugetlb_vmemmap remap. Nothing frees those ranges concurrently, so they cannot race and do not need RCU. ptdump is the only walker that traverses ranges it does not own.
Defer the free by an RCU grace period. pagetable_free_kernel() now frees via call_rcu() in both the async and non-async configs. The async path still flushes the TLB first, then queues the per-page RCU free. The page stays valid until any walk that may have observed it drops its RCU read lock.
On the read side ptdump_walk_pgd() takes the RCU read lock around the walk, and walk_page_range_debug() asserts it with RCU_LOCKDEP_WARN() for the init_mm case rather than taking it, matching pagewalk.c convention. A walker either sees the cleared PMD and skips, or keeps the page alive until it drops the lock. The owned-range walkers are unchanged.
ptdump callbacks now run under RCU, so they must not sleep. The arch note_page() and effective_prot() callbacks only format into the preallocated seq_file buffer, and the walker does not call cond_resched(); the only GFP_KERNEL marker setup runs before the walk.
---
v5: reframe changelog around the pre-existing race and range ownership;
correct the mmap-lock description (arm64 is the exception, not x86); move rcu_read_lock() into ptdump_walk_pgd() and assert it in walk_page_range_debug(); drop walk_kernel_page_table_range_rcu(); fix the pgtable-generic.c comment; document the no-sleep audit of the callbacks.
v4: defer the free in both the async and non async configs, not just
the async one. Move the walk under a named walk_kernel_page_table_range_rcu() helper instead of open coding rcu_read_lock() in walk_page_range_debug().
v3: take rcu_read_lock() in the init_mm branch of
walk_page_range_debug() rather than inside the lockless walker, which the arm64 split paths also use with GFP_PGTABLE_KERNEL and can sleep.
v2: use call_rcu() instead of synchronize_rcu().
--- include/linux/mm.h | 7 ------- mm/pagewalk.c | 14 +++++++++----- mm/pgtable-generic.c | 22 +++++++++++++++++++++- mm/ptdump.c | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-)
include/linux/mm.h +0 -7
@@ -3695,14 +3695,7 @@ static inline void __pagetable_free(struct ptdesc *pt)__free_pages (page,compound_order (page)); } -#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE void pagetable_free_kernel (struct ptdesc *pt); -#else -static inline void pagetable_free_kernel (struct ptdesc *pt) -{ -__pagetable_free (pt); -} -#endif /** * pagetable_free - Free pagetables * @pt: The page table descriptor
mm/pagewalk.c +9 -5
@@ -620,7 +620,7 @@ int walk_page_range(struct mm_struct *mm, unsigned long start,* Note: Be careful to walk the kernel pages tables, the caller may be need to * take other effective approaches (mmap lock may be insufficient) to prevent * the intermediate kernel page tables belonging to the specified address range -* from being freed (e.g. memory hot-remove). +* from being freed (e.g. memory hot-remove, vmap huge page promotion). */ int walk_kernel_page_table_range (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)
@@ -643,7 +643,7 @@ int walk_kernel_page_table_range(unsigned long start, unsigned long end,* Use this function to walk the kernel page tables locklessly. It should be * guaranteed that the caller has exclusive access over the range they are * operating on - that there should be no concurrent access, for example, -* changing permissions for vmalloc objects. +* changing permissions for vmalloc objects, or vmap huge page promotion. */ int walk_kernel_page_table_range_lockless (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)
@@ -692,9 +692,13 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start, };/* For convenience, we allow traversal of kernel mappings. */ -if (mm == &init_mm) -return walk_kernel_page_table_range (start, end, ops, - pgd, private); +if (mm == &init_mm) { +RCU_LOCKDEP_WARN (!rcu_read_lock_held (), +"RCU read lock must be held across kernel page table walk" ); +return walk_kernel_page_table_range (start, end, ops, pgd, + private); + } +if (start >= end || !walk.mm)return -EINVAL ;if (!check_ops_safe (ops))
mm/pgtable-generic.c +21 -1
@@ -410,6 +410,13 @@ pte_t *pte_offset_map_lock(struct mm_struct *mm, pmd_t *pmd, goto again; } +static void kernel_pgtable_free_rcu (struct rcu_head *head) +{ + struct ptdesc *pt = container_of(head, struct ptdesc, pt_rcu_head); + +__pagetable_free (pt); +} +#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE static void kernel_pgtable_work_func (struct work_struct *work);
@@ -434,8 +441,15 @@ static void kernel_pgtable_work_func(struct work_struct *work)spin_unlock (&kernel_pgtable_work.lock);iommu_sva_invalidate_kva_range (PAGE_OFFSET ,TLB_FLUSH_ALL ); + +/* +* Debug walkers (ptdump) may walk ranges they do not own and race this +* free, so they walk under rcu_read_lock(). Free after a grace period: +* a walker either already saw the cleared PMD, or keeps the page alive +* until it drops the RCU lock. +*/ list_for_each_entry_safe (pt, next, &page_list, pt_list) -__pagetable_free (pt); +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); }void pagetable_free_kernel (struct ptdesc *pt)
@@ -446,4 +460,10 @@ void pagetable_free_kernel(struct ptdesc *pt) schedule_work(&kernel_pgtable_work.work); } +#else +void pagetable_free_kernel (struct ptdesc *pt) +{ +/* Defer the free by a grace period; see kernel_pgtable_work_func(). */ +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); +}#endif
mm/ptdump.c +2 -0
@@ -178,11 +178,13 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)get_online_mems ();mmap_write_lock (mm); +rcu_read_lock ();while (range->start != range->end) {walk_page_range_debug (mm, range->start, range->end, &ptdump_ops, pgd, st); range++; } +rcu_read_unlock ();mmap_write_unlock (mm);put_online_mems ();
Signature · 1 line
2.53.0
Quoted 37 lines · David Carlier
ptdump_walk_pgd()walks the kernel page tables underget_online_mems()andmmap_write_lock(&init_mm). Neither lock stops vmalloc from freeing a kernel PTE page underneath the walk.When
vmap_try_huge_pmd()promotes a range to a huge PMD it collapses the existing PTE table and frees it viapmd_free_pte_page(). On x86, riscv and powerpc this runs without theinit_mmmmap lock; only arm64 takes it, and not on the block-split path. So ptdump can dereference a just-freed PTE page, which is the use after free syzbot hit inptdump_pte_entry().The race is not new. ptdump walks the whole kernel address space, including ranges other code is actively mapping, so it reads page tables it does not own.
5ba2f0a15564("mm: introduce deferred freeing for kernel page tables") only widened the window; the Fixes tag points there for that reason.Every other walker works on a range it owns and is the only one mutating it:
set_memory()on arm64/riscv/loongarch, the arm64 block-split path, the openrisc DMA path and thehugetlb_vmemmapremap. Nothing frees those ranges concurrently, so they cannot race and do not need RCU. ptdump is the only walker that traverses ranges it does not own.Defer the free by an RCU grace period.
pagetable_free_kernel()now frees viacall_rcu()in both the async and non-async configs. The async path still flushes the TLB first, then queues the per-page RCU free. The page stays valid until any walk that may have observed it drops its RCU read lock.On the read side
ptdump_walk_pgd()takes the RCU read lock around the walk, andwalk_page_range_debug()asserts it withRCU_LOCKDEP_WARN()for theinit_mmcase rather than taking it, matchingpagewalk.cconvention. A walker either sees the cleared PMD and skips, or keeps the page alive until it drops the lock. The owned-range walkers are unchanged.ptdump callbacks now run under RCU, so they must not sleep. The arch
note_page()andeffective_prot()callbacks only format into the preallocatedseq_filebuffer, and the walker does not callcond_resched(); the onlyGFP_KERNELmarker setup runs before the walk.
Quoted 6 lines
Fixes 5ba2f0a15564 ("mm: introduce deferred freeing for kernel page tables")Reported-by [email protected]Assisted-by Claude:claude-opus-4-8Signed-off-by David Carlier <[email protected]>---
Thanks.
Is my assessment correct that this patch deems commit fa93b45fd397 ("arm64: Enable vmalloc-huge with ptdump") redundant? (in which case I can send a partial revert for that)
Quoted 130 lines
v5: reframe changelog around the pre-existing race and range ownership;
correct the mmap-lock description (arm64 is the exception, not x86); movercu_read_lock()intoptdump_walk_pgd()and assert it inwalk_page_range_debug(); dropwalk_kernel_page_table_range_rcu(); fix thepgtable-generic.ccomment; document the no-sleep audit of the callbacks.
v4: defer the free in both the async and non async configs, not just
the async one. Move the walk under a namedwalk_kernel_page_table_range_rcu()helper instead of open codingrcu_read_lock()inwalk_page_range_debug().
v3: takercu_read_lock()in theinit_mmbranch of
walk_page_range_debug()rather than inside the lockless walker, which the arm64 split paths also use withGFP_PGTABLE_KERNELand can sleep.
v2: usecall_rcu()instead ofsynchronize_rcu().--- include/linux/mm.h | 7 ------- mm/pagewalk.c | 14 +++++++++----- mm/pgtable-generic.c | 22 +++++++++++++++++++++- mm/ptdump.c | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-)include/linux/mm.h +0
-7@@ -3695,14 +3695,7 @@ static inline void __pagetable_free(struct ptdesc *pt)__free_pages (page,compound_order (page)); } -#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE void pagetable_free_kernel (struct ptdesc *pt); -#else -static inline void pagetable_free_kernel (struct ptdesc *pt) -{ -__pagetable_free (pt); -} -#endif /** * pagetable_free - Free pagetables * @pt: The page table descriptor mm/pagewalk.c +9
-5@@ -620,7 +620,7 @@ int walk_page_range(struct mm_struct *mm, unsigned long start,* Note: Be careful to walk the kernel pages tables, the caller may be need to * take other effective approaches (mmap lock may be insufficient) to prevent * the intermediate kernel page tables belonging to the specified address range -* from being freed (e.g. memory hot-remove). +* from being freed (e.g. memory hot-remove, vmap huge page promotion). */ int walk_kernel_page_table_range (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -643,7 +643,7 @@ int walk_kernel_page_table_range(unsigned long start, unsigned long end,* Use this function to walk the kernel page tables locklessly. It should be * guaranteed that the caller has exclusive access over the range they are * operating on - that there should be no concurrent access, for example, -* changing permissions for vmalloc objects. +* changing permissions for vmalloc objects, or vmap huge page promotion. */ int walk_kernel_page_table_range_lockless (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -692,9 +692,13 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start, };/* For convenience, we allow traversal of kernel mappings. */ -if (mm == &init_mm) -return walk_kernel_page_table_range (start, end, ops, - pgd, private); +if (mm == &init_mm) { +RCU_LOCKDEP_WARN (!rcu_read_lock_held (), +"RCU read lock must be held across kernel page table walk" ); +return walk_kernel_page_table_range (start, end, ops, pgd, + private); + } +if (start >= end || !walk.mm)return -EINVAL ;if (!check_ops_safe (ops))mm/pgtable-generic.c +21
-1@@ -410,6 +410,13 @@ pte_t *pte_offset_map_lock(struct mm_struct *mm, pmd_t *pmd, goto again; } +static void kernel_pgtable_free_rcu (struct rcu_head *head) +{ + struct ptdesc *pt = container_of(head, struct ptdesc, pt_rcu_head); + +__pagetable_free (pt); +} +#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE static void kernel_pgtable_work_func (struct work_struct *work);@@ -434,8 +441,15 @@ static void kernel_pgtable_work_func(struct work_struct *work)spin_unlock (&kernel_pgtable_work.lock);iommu_sva_invalidate_kva_range (PAGE_OFFSET ,TLB_FLUSH_ALL ); + +/* +* Debug walkers (ptdump) may walk ranges they do not own and race this +* free, so they walk under rcu_read_lock(). Free after a grace period: +* a walker either already saw the cleared PMD, or keeps the page alive +* until it drops the RCU lock. +*/ list_for_each_entry_safe (pt, next, &page_list, pt_list) -__pagetable_free (pt); +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); }void pagetable_free_kernel (struct ptdesc *pt)@@ -446,4 +460,10 @@ void pagetable_free_kernel(struct ptdesc *pt) schedule_work(&kernel_pgtable_work.work); } +#else +void pagetable_free_kernel (struct ptdesc *pt) +{ +/* Defer the free by a grace period; see kernel_pgtable_work_func(). */ +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); +}#endif mm/ptdump.c +0
-0
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 180
Quoted 3 lines
mm/ptdump.c +0
-0@@ -178,11 +178,13 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)get_online_mems ();mmap_write_lock (mm);
mmap write lock is not required anymore no?
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 181
Quoted 1 line
mm/ptdump.c +1
-0+rcu_read_lock ();
Is there a possibility of RCU stall here? For example a system with lots of physical memory will have a huge linear map.
Quoted 9 lines
mm/ptdump.c +1
-0while (range->start != range->end) {walk_page_range_debug (mm, range->start, range->end, &ptdump_ops, pgd, st); range++; } +rcu_read_unlock ();mmap_write_unlock (mm);put_online_mems ();
Hi Dev,
Yes, the RCU deferral covers arm64 too, so the static key and the mmap_lock in pud_free_pmd_page()/pmd_free_pte_page() are now redundant. Keep the arch_vmap_*_supported() changes though.
Cheers.
Quoted 207 lines · Dev Jain
Quoted 37 lines · David Carlier
ptdump_walk_pgd()walks the kernel page tables underget_online_mems()andmmap_write_lock(&init_mm). Neither lock stops vmalloc from freeing a kernel PTE page underneath the walk.When
vmap_try_huge_pmd()promotes a range to a huge PMD it collapses the existing PTE table and frees it viapmd_free_pte_page(). On x86, riscv and powerpc this runs without theinit_mmmmap lock; only arm64 takes it, and not on the block-split path. So ptdump can dereference a just-freed PTE page, which is the use after free syzbot hit inptdump_pte_entry().The race is not new. ptdump walks the whole kernel address space, including ranges other code is actively mapping, so it reads page tables it does not own.
5ba2f0a15564("mm: introduce deferred freeing for kernel page tables") only widened the window; the Fixes tag points there for that reason.Every other walker works on a range it owns and is the only one mutating it:
set_memory()on arm64/riscv/loongarch, the arm64 block-split path, the openrisc DMA path and thehugetlb_vmemmapremap. Nothing frees those ranges concurrently, so they cannot race and do not need RCU. ptdump is the only walker that traverses ranges it does not own.Defer the free by an RCU grace period.
pagetable_free_kernel()now frees viacall_rcu()in both the async and non-async configs. The async path still flushes the TLB first, then queues the per-page RCU free. The page stays valid until any walk that may have observed it drops its RCU read lock.On the read side
ptdump_walk_pgd()takes the RCU read lock around the walk, andwalk_page_range_debug()asserts it withRCU_LOCKDEP_WARN()for theinit_mmcase rather than taking it, matchingpagewalk.cconvention. A walker either sees the cleared PMD and skips, or keeps the page alive until it drops the lock. The owned-range walkers are unchanged.ptdump callbacks now run under RCU, so they must not sleep. The arch
note_page()andeffective_prot()callbacks only format into the preallocatedseq_filebuffer, and the walker does not callcond_resched(); the onlyGFP_KERNELmarker setup runs before the walk.Quoted 6 lines
Fixes 5ba2f0a15564 ("mm: introduce deferred freeing for kernel page tables")Reported-by [email protected]Assisted-by Claude:claude-opus-4-8Signed-off-by David Carlier <[email protected]>---
Thanks.
Is my assessment correct that this patch deems commit
fa93b45fd397("arm64: Enable vmalloc-huge with ptdump") redundant? (in which case I can send a partial revert for that)Quoted 130 lines
v5: reframe changelog around the pre-existing race and range ownership;
correct the mmap-lock description (arm64 is the exception, not x86); movercu_read_lock()intoptdump_walk_pgd()and assert it inwalk_page_range_debug(); dropwalk_kernel_page_table_range_rcu(); fix thepgtable-generic.ccomment; document the no-sleep audit of the callbacks.
v4: defer the free in both the async and non async configs, not just
the async one. Move the walk under a namedwalk_kernel_page_table_range_rcu()helper instead of open codingrcu_read_lock()inwalk_page_range_debug().
v3: takercu_read_lock()in theinit_mmbranch of
walk_page_range_debug()rather than inside the lockless walker, which the arm64 split paths also use withGFP_PGTABLE_KERNELand can sleep.
v2: usecall_rcu()instead ofsynchronize_rcu().--- include/linux/mm.h | 7 ------- mm/pagewalk.c | 14 +++++++++----- mm/pgtable-generic.c | 22 +++++++++++++++++++++- mm/ptdump.c | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-)include/linux/mm.h +0
-7@@ -3695,14 +3695,7 @@ static inline void __pagetable_free(struct ptdesc *pt)__free_pages (page,compound_order (page)); } -#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE void pagetable_free_kernel (struct ptdesc *pt); -#else -static inline void pagetable_free_kernel (struct ptdesc *pt) -{ -__pagetable_free (pt); -} -#endif /** * pagetable_free - Free pagetables * @pt: The page table descriptor mm/pagewalk.c +9
-5@@ -620,7 +620,7 @@ int walk_page_range(struct mm_struct *mm, unsigned long start,* Note: Be careful to walk the kernel pages tables, the caller may be need to * take other effective approaches (mmap lock may be insufficient) to prevent * the intermediate kernel page tables belonging to the specified address range -* from being freed (e.g. memory hot-remove). +* from being freed (e.g. memory hot-remove, vmap huge page promotion). */ int walk_kernel_page_table_range (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -643,7 +643,7 @@ int walk_kernel_page_table_range(unsigned long start, unsigned long end,* Use this function to walk the kernel page tables locklessly. It should be * guaranteed that the caller has exclusive access over the range they are * operating on - that there should be no concurrent access, for example, -* changing permissions for vmalloc objects. +* changing permissions for vmalloc objects, or vmap huge page promotion. */ int walk_kernel_page_table_range_lockless (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -692,9 +692,13 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start, };/* For convenience, we allow traversal of kernel mappings. */ -if (mm == &init_mm) -return walk_kernel_page_table_range (start, end, ops, - pgd, private); +if (mm == &init_mm) { +RCU_LOCKDEP_WARN (!rcu_read_lock_held (), +"RCU read lock must be held across kernel page table walk" ); +return walk_kernel_page_table_range (start, end, ops, pgd, + private); + } +if (start >= end || !walk.mm)return -EINVAL ;if (!check_ops_safe (ops))mm/pgtable-generic.c +21
-1@@ -410,6 +410,13 @@ pte_t *pte_offset_map_lock(struct mm_struct *mm, pmd_t *pmd, goto again; } +static void kernel_pgtable_free_rcu (struct rcu_head *head) +{ + struct ptdesc *pt = container_of(head, struct ptdesc, pt_rcu_head); + +__pagetable_free (pt); +} +#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE static void kernel_pgtable_work_func (struct work_struct *work);@@ -434,8 +441,15 @@ static void kernel_pgtable_work_func(struct work_struct *work)spin_unlock (&kernel_pgtable_work.lock);iommu_sva_invalidate_kva_range (PAGE_OFFSET ,TLB_FLUSH_ALL ); + +/* +* Debug walkers (ptdump) may walk ranges they do not own and race this +* free, so they walk under rcu_read_lock(). Free after a grace period: +* a walker either already saw the cleared PMD, or keeps the page alive +* until it drops the RCU lock. +*/ list_for_each_entry_safe (pt, next, &page_list, pt_list) -__pagetable_free (pt); +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); }void pagetable_free_kernel (struct ptdesc *pt)@@ -446,4 +460,10 @@ void pagetable_free_kernel(struct ptdesc *pt) schedule_work(&kernel_pgtable_work.work); } +#else +void pagetable_free_kernel (struct ptdesc *pt) +{ +/* Defer the free by a grace period; see kernel_pgtable_work_func(). */ +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); +}#endif mm/ptdump.c +0
-0mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 180
Quoted 3 lines
mm/ptdump.c +0
-0@@ -178,11 +178,13 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)get_online_mems ();mmap_write_lock (mm);mmap write lock is not required anymore no?
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 181
Quoted 1 line
mm/ptdump.c +1
-0+rcu_read_lock ();Is there a possibility of RCU stall here? For example a system with lots of physical memory will have a huge linear map.
Quoted 9 lines
mm/ptdump.c +1
-0while (range->start != range->end) {walk_page_range_debug (mm, range->start, range->end, &ptdump_ops, pgd, st); range++; } +rcu_read_unlock ();mmap_write_unlock (mm);put_online_mems ();
Quoted 5 lines · David CARLIER
Hi Dev,
Yes, the RCU deferral covers arm64 too, so the static key and the
mmap_lockin pud_free_pmd_page()/pmd_free_pte_page() are now redundant. Keep the arch_vmap_*_supported() changes though.
Sorry I should have mentioned in my previous email that I had a comment below, on the code :) could you please see below.
[--- snip ---]
Quoted 10 lines
Quoted 10 lines
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 180
Quoted 8 lines
mm/ptdump.c +0
-0@@ -178,11 +178,13 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)get_online_mems ();mmap_write_lock (mm);mmap write lock is not required anymore no?
^
Quoted 6 lines
Quoted 6 lines
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 181
Quoted 3 lines
mm/ptdump.c +1
-0+rcu_read_lock ();Is there a possibility of RCU stall here? For example a system with lots of physical memory will have a huge linear map.
^
Quoted 11 lines
Quoted 11 lines
Quoted 10 lines
mm/ptdump.c +1
-0while (range->start != range->end) {walk_page_range_debug (mm, range->start, range->end, &ptdump_ops, pgd, st); range++; } +rcu_read_unlock ();mmap_write_unlock (mm);put_online_mems ();
David Hildenbrand (Arm)
maintainerQuoted 3 lines · David Carlier
ptdump_walk_pgd()walks the kernel page tables underget_online_mems()andmmap_write_lock(&init_mm). Neither lock stops vmalloc from freeing a kernel PTE page underneath the walk.
I guess other directmap modifications would similarly be problematic (assuming we'd collapse into PMDs for reducing directmap fragmentation, I think some patches are on the list for that).
Not sure if all of these would go through pagetable_free_kernel(), given that we have memblock-allocated page tables that are freed entirely differently.
Quoted 67 lines
When
vmap_try_huge_pmd()promotes a range to a huge PMD it collapses the existing PTE table and frees it viapmd_free_pte_page(). On x86, riscv and powerpc this runs without theinit_mmmmap lock; only arm64 takes it, and not on the block-split path. So ptdump can dereference a just-freed PTE page, which is the use after free syzbot hit inptdump_pte_entry().The race is not new. ptdump walks the whole kernel address space, including ranges other code is actively mapping, so it reads page tables it does not own.
5ba2f0a15564("mm: introduce deferred freeing for kernel page tables") only widened the window; the Fixes tag points there for that reason.Every other walker works on a range it owns and is the only one mutating it:
set_memory()on arm64/riscv/loongarch, the arm64 block-split path, the openrisc DMA path and thehugetlb_vmemmapremap. Nothing frees those ranges concurrently, so they cannot race and do not need RCU. ptdump is the only walker that traverses ranges it does not own.Defer the free by an RCU grace period.
pagetable_free_kernel()now frees viacall_rcu()in both the async and non-async configs. The async path still flushes the TLB first, then queues the per-page RCU free. The page stays valid until any walk that may have observed it drops its RCU read lock.On the read side
ptdump_walk_pgd()takes the RCU read lock around the walk, andwalk_page_range_debug()asserts it withRCU_LOCKDEP_WARN()for theinit_mmcase rather than taking it, matchingpagewalk.cconvention. A walker either sees the cleared PMD and skips, or keeps the page alive until it drops the lock. The owned-range walkers are unchanged.ptdump callbacks now run under RCU, so they must not sleep. The arch
note_page()andeffective_prot()callbacks only format into the preallocatedseq_filebuffer, and the walker does not callcond_resched(); the onlyGFP_KERNELmarker setup runs before the walk.Fixes 5ba2f0a15564 ("mm: introduce deferred freeing for kernel page tables")Reported-by [email protected]Assisted-by Claude:claude-opus-4-8Signed-off-by David Carlier <[email protected]>---
v5: reframe changelog around the pre-existing race and range ownership;
correct the mmap-lock description (arm64 is the exception, not x86); movercu_read_lock()intoptdump_walk_pgd()and assert it inwalk_page_range_debug(); dropwalk_kernel_page_table_range_rcu(); fix thepgtable-generic.ccomment; document the no-sleep audit of the callbacks.
v4: defer the free in both the async and non async configs, not just
the async one. Move the walk under a namedwalk_kernel_page_table_range_rcu()helper instead of open codingrcu_read_lock()inwalk_page_range_debug().
v3: takercu_read_lock()in theinit_mmbranch of
walk_page_range_debug()rather than inside the lockless walker, which the arm64 split paths also use withGFP_PGTABLE_KERNELand can sleep.
v2: usecall_rcu()instead ofsynchronize_rcu().--- include/linux/mm.h | 7 ------- mm/pagewalk.c | 14 +++++++++----- mm/pgtable-generic.c | 22 +++++++++++++++++++++- mm/ptdump.c | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-)include/linux/mm.h +0
-0@@ -3695,14 +3695,7 @@ static inline void __pagetable_free(struct ptdesc *pt)__free_pages (page,compound_order (page));
include/linux/mm.h static inline void __pagetable_free(struct ptdesc *pt) · line 3705
Quoted 10 lines
include/linux/mm.h +0
-7} -#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE void pagetable_free_kernel (struct ptdesc *pt); -#else -static inline void pagetable_free_kernel (struct ptdesc *pt) -{ -__pagetable_free (pt); -} -#endif
Okay, now we always incur a function call overhead. I guess it will be fine, just saying.
Quoted 93 lines
include/linux/mm.h +0
-0/** * pagetable_free - Free pagetables * @pt: The page table descriptor mm/pagewalk.c +9
-5@@ -620,7 +620,7 @@ int walk_page_range(struct mm_struct *mm, unsigned long start,* Note: Be careful to walk the kernel pages tables, the caller may be need to * take other effective approaches (mmap lock may be insufficient) to prevent * the intermediate kernel page tables belonging to the specified address range -* from being freed (e.g. memory hot-remove). +* from being freed (e.g. memory hot-remove, vmap huge page promotion). */ int walk_kernel_page_table_range (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -643,7 +643,7 @@ int walk_kernel_page_table_range(unsigned long start, unsigned long end,* Use this function to walk the kernel page tables locklessly. It should be * guaranteed that the caller has exclusive access over the range they are * operating on - that there should be no concurrent access, for example, -* changing permissions for vmalloc objects. +* changing permissions for vmalloc objects, or vmap huge page promotion. */ int walk_kernel_page_table_range_lockless (unsigned long start,unsigned long end,const struct mm_walk_ops *ops,pgd_t *pgd,void *private)@@ -692,9 +692,13 @@ int walk_page_range_debug(struct mm_struct *mm, unsigned long start, };/* For convenience, we allow traversal of kernel mappings. */ -if (mm == &init_mm) -return walk_kernel_page_table_range (start, end, ops, - pgd, private); +if (mm == &init_mm) { +RCU_LOCKDEP_WARN (!rcu_read_lock_held (), +"RCU read lock must be held across kernel page table walk" ); +return walk_kernel_page_table_range (start, end, ops, pgd, + private); + } +if (start >= end || !walk.mm)return -EINVAL ;if (!check_ops_safe (ops))mm/pgtable-generic.c +21
-1@@ -410,6 +410,13 @@ pte_t *pte_offset_map_lock(struct mm_struct *mm, pmd_t *pmd, goto again; } +static void kernel_pgtable_free_rcu (struct rcu_head *head) +{ + struct ptdesc *pt = container_of(head, struct ptdesc, pt_rcu_head); + +__pagetable_free (pt); +} +#ifdef CONFIG_ASYNC_KERNEL_PGTABLE_FREE static void kernel_pgtable_work_func (struct work_struct *work);@@ -434,8 +441,15 @@ static void kernel_pgtable_work_func(struct work_struct *work)spin_unlock (&kernel_pgtable_work.lock);iommu_sva_invalidate_kva_range (PAGE_OFFSET ,TLB_FLUSH_ALL ); + +/* +* Debug walkers (ptdump) may walk ranges they do not own and race this +* free, so they walk under rcu_read_lock(). Free after a grace period: +* a walker either already saw the cleared PMD, or keeps the page alive +* until it drops the RCU lock. +*/ list_for_each_entry_safe (pt, next, &page_list, pt_list) -__pagetable_free (pt); +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); }void pagetable_free_kernel (struct ptdesc *pt)@@ -446,4 +460,10 @@ void pagetable_free_kernel(struct ptdesc *pt) schedule_work(&kernel_pgtable_work.work); } +#else +void pagetable_free_kernel (struct ptdesc *pt) +{ +/* Defer the free by a grace period; see kernel_pgtable_work_func(). */ +call_rcu (&pt->pt_rcu_head, kernel_pgtable_free_rcu); +}#endif mm/ptdump.c +0
-0
mm/ptdump.c void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd) · line 187
Quoted 10 lines
mm/ptdump.c +2
-0@@ -178,11 +178,13 @@ void ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm, pgd_t *pgd)get_online_mems ();mmap_write_lock (mm); +rcu_read_lock ();while (range->start != range->end) {walk_page_range_debug (mm, range->start, range->end, &ptdump_ops, pgd, st); range++; } +rcu_read_unlock ();
Any reason that is not done inside walk_page_range_debug() ? Dropping the lock in between should not be a concern?
Signature · 3 lines
Cheers, David