mm: khugepaged: fix tracepoint UAF
Subsystems (4)
v5 cover mm: khugepaged: fix tracepoint UAF
From: Vernon Yang <[email protected]>
The khugepaged tracepoints take a folio pointer and call folio_pfn(), but by then the folio may no longer be valid: freed after folio_put(), folio_unlock() or pte_unmap_unlock(), or not a folio at all but an xarray-encoded swap entry. On classic SPARSEMEM, dereferencing it oopses khugepaged as soon as the trace event is enabled; on other memory models it merely prints a bogus pfn.
Pass the pfn to the tracepoints directly, captured while the folio is still pinned, closing the use-after-free windows in mm_khugepaged_scan_file(), mm_khugepaged_scan_pmd() and mm_khugepaged_collapse_file().
This series is based on mm-new + revert v4.
V4 -> V5:
- Use a single trace statement.
- Collect Acked-by.
V3 -> V4:
- Only trace the PFN if it really was problematic.
- Calling the respective trace_xxx() functions separately on success and failure.
- Set new_pfn once after successful alloc_charge_folio().
V2 -> V3:
- Place folio_pfn() inside the xas_for_each() loop in PATCH#1.
- Already defaulted the pfn value to -1, to simple it in PATCH#2.
V1 -> V2:
- Instead of passing the folio, just pass the pfn directly.
- Using the folio_pfn() before dropping the reference or the page table lock.
V4 : https://lore.kernel.org/linux-mm/[email protected]/
V3 : https://lore.kernel.org/linux-mm/[email protected]/
V2 : https://lore.kernel.org/linux-mm/[email protected]/
V1 : https://lore.kernel.org/linux-mm/[email protected]/
Vernon Yang (3):
mm: khugepaged: fix swap entry value to folio_pfn()
mm: khugepaged: fix folio is used after pte_unmap_unlock()
mm: khugepaged: fix folio is used after folio_put/unlock()
include/trace/events/huge_memory.h | 18 +++++++++--------- mm/khugepaged.c | 21 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 12 deletions(-)
--
2.53.0
PATCH v5 1/3 mm: khugepaged: fix swap entry value to folio_pfn()
From: Vernon Yang <[email protected]>
When the swap entries found exceed max_ptes_swap, the loop is left via break with folio still holding the xarray value that encodes the swap entry, not valid folio pointer.
That value is passed to trace_mm_khugepaged_scan_file(), which feeds it to folio_pfn(). On FLATMEM and SPARSEMEM_VMEMMAP, the page_to_pfn() is plain pointer arithmetic, so the trace event merely prints bogus scan_pfn. On classic SPARSEMEM, the page_to_pfn() reads page->flags, dereferencing the tiny encoded integer and oopsing khugepaged whenever the trace event is enabled.
So when folio is the swap entry value, simply set pfn to -1, just like exhausted scan naturally.
And the folio_put() has maybe dropped the last reference of folio. The trace_mm_khugepaged_scan_file() is left with a dangling folio pointer. so using the folio_pfn() before dropping the reference, closing use-after-free window.
--- include/trace/events/huge_memory.h | 6 +++--- mm/khugepaged.c | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-)
include/trace/events/huge_memory.h +3 -3
@@ -178,10 +178,10 @@ TRACE_EVENT(mm_collapse_huge_page_swapin, TRACE_EVENT(mm_khugepaged_scan_file, - TP_PROTO(struct mm_struct *mm, struct folio *folio, struct file *file, + TP_PROTO(struct mm_struct *mm, unsigned long pfn, struct file *file, int present, int swap, int result), - TP_ARGS(mm, folio, file, present, swap, result), + TP_ARGS(mm, pfn, file, present, swap, result), TP_STRUCT__entry( __field(struct mm_struct *, mm)
@@ -194,7 +194,7 @@ TRACE_EVENT(mm_khugepaged_scan_file,TP_fast_assign ( __entry->mm = mm; - __entry->pfn = folio ?folio_pfn (folio) :-1 ; + __entry->pfn = pfn;__assign_str (filename); __entry->present = present; __entry->swap = swap;
mm/khugepaged.c +6 -1
@@ -2690,6 +2690,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,int present, swap;int node =NUMA_NO_NODE ;enum scan_result result =SCAN_SUCCEED ; +unsigned long failed_pfn =-1 ; present =0 ; swap =0 ;
@@ -2722,6 +2723,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,if (is_pmd_order (folio_order (folio))) { result =SCAN_PTE_MAPPED_HUGEPAGE ; + failed_pfn =folio_pfn (folio);/* * PMD-sized THP implies that we can only try * retracting the PTE table.
@@ -2733,6 +2735,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm, node =folio_nid (folio);if (collapse_scan_abort (node, cc)) { result =SCAN_SCAN_ABORT ; + failed_pfn =folio_pfn (folio);folio_put (folio);break ; }
@@ -2740,12 +2743,14 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,if (!folio_test_lru (folio)) { result =SCAN_PAGE_LRU ; + failed_pfn =folio_pfn (folio);folio_put (folio);break ; }if (folio_expected_ref_count (folio) +1 !=folio_ref_count (folio)) { result =SCAN_PAGE_COUNT ; + failed_pfn =folio_pfn (folio);folio_put (folio);break ; }
@@ -2780,7 +2785,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm, } } -trace_mm_khugepaged_scan_file (mm, folio, file, present, swap, result); +trace_mm_khugepaged_scan_file (mm, failed_pfn, file, present, swap, result);return result; }
Signature · 1 line
2.53.0
PATCH v5 2/3 mm: khugepaged: fix folio is used after pte_unmap_unlock()
In reply to Vernon YangFrom: Vernon Yang <[email protected]>
After the page table lock has dropped, the folio can be freed concurrently. The trace_mm_khugepaged_scan_pmd() is left with a dangling folio pointer.
So using the folio_pfn() before dropping the page table lock, closing use-after-free window.
And other pre-existing bug, When the for (i = 0; i < HPAGE_PMD_NR; i++) iteration to terminate and the folio operation preceding is normal, but pfn will be incorrect. so we really only trace the PFN if it really was problematic.
--- include/trace/events/huge_memory.h | 6 +++--- mm/khugepaged.c | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-)
include/trace/events/huge_memory.h +3 -3
@@ -55,10 +55,10 @@ SCAN_STATUS TRACE_EVENT(mm_khugepaged_scan_pmd, - TP_PROTO(struct mm_struct *mm, struct folio *folio, + TP_PROTO(struct mm_struct *mm, unsigned long pfn, int referenced, int none_or_zero, int status, int unmapped), - TP_ARGS(mm, folio, referenced, none_or_zero, status, unmapped), + TP_ARGS(mm, pfn, referenced, none_or_zero, status, unmapped), TP_STRUCT__entry( __field(struct mm_struct *, mm)
@@ -71,7 +71,7 @@ TRACE_EVENT(mm_khugepaged_scan_pmd,TP_fast_assign ( __entry->mm = mm; - __entry->pfn = folio ?folio_pfn (folio) :-1 ; + __entry->pfn = pfn; __entry->referenced = referenced; __entry->none_or_zero = none_or_zero; __entry->status = status;
mm/khugepaged.c +9 -1
@@ -1612,6 +1612,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,enum scan_result result =SCAN_FAIL ;struct page *page =NULL ;struct folio *folio =NULL ; +unsigned long failed_pfn =-1 ;unsigned long addr;unsigned long enabled_orders;spinlock_t *ptl;
@@ -1706,11 +1707,13 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,if (cc->is_khugepaged && !(vma->vm_flags &VM_DROPPABLE ) &&folio_test_lazyfree (folio) && !pte_dirty (pteval)) { result =SCAN_PAGE_LAZYFREE ; + failed_pfn =folio_pfn (folio); goto out_unmap; }if (!folio_test_anon (folio)) { result =SCAN_PAGE_ANON ; + failed_pfn =folio_pfn (folio); goto out_unmap; }
@@ -1721,6 +1724,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,if (folio_maybe_mapped_shared (folio)) {if (++shared > max_ptes_shared) { result =SCAN_EXCEED_SHARED_PTE ; + failed_pfn =folio_pfn (folio);count_collapse_event (HPAGE_PMD_ORDER ,THP_SCAN_EXCEED_SHARED_PTE ,MTHP_STAT_COLLAPSE_EXCEED_SHARED ); goto out_unmap;
@@ -1738,15 +1742,18 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm, node =folio_nid (folio);if (collapse_scan_abort (node, cc)) { result =SCAN_SCAN_ABORT ; + failed_pfn =folio_pfn (folio); goto out_unmap; } cc->node_load[node]++;if (!folio_test_lru (folio)) { result =SCAN_PAGE_LRU ; + failed_pfn =folio_pfn (folio); goto out_unmap; }if (folio_test_locked (folio)) { result =SCAN_PAGE_LOCK ; + failed_pfn =folio_pfn (folio); goto out_unmap; }
@@ -1759,6 +1766,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,*/ if (folio_expected_ref_count (folio) !=folio_ref_count (folio)) { result =SCAN_PAGE_COUNT ; + failed_pfn =folio_pfn (folio); goto out_unmap; }
@@ -1784,7 +1792,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm, *lock_dropped = true; } out: -trace_mm_khugepaged_scan_pmd (mm, folio, referenced, +trace_mm_khugepaged_scan_pmd (mm, failed_pfn, referenced, none_or_zero, result, unmapped);return result; }
Signature · 1 line
2.53.0
PATCH v5 3/3 mm: khugepaged: fix folio is used after folio_put/unlock()
In reply to Vernon YangFrom: Vernon Yang <[email protected]>
On the rollback path, folio_put() has already dropped the last reference of new_folio. On the success path, new_folio is already unlocked and can be freed concurrently. The trace_mm_khugepaged_collapse_file() is left with a dangling folio pointer.
So using the folio_pfn() before dropping the reference, closing use-after-free window.
--- include/trace/events/huge_memory.h | 6 +++--- mm/khugepaged.c | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-)
include/trace/events/huge_memory.h +3 -3
@@ -211,10 +211,10 @@ TRACE_EVENT(mm_khugepaged_scan_file, ); TRACE_EVENT(mm_khugepaged_collapse_file, - TP_PROTO(struct mm_struct *mm, struct folio *new_folio, pgoff_t index, + TP_PROTO(struct mm_struct *mm, unsigned long new_pfn, pgoff_t index, unsigned long addr, bool is_shmem, struct file *file, int nr, int result), - TP_ARGS(mm, new_folio, index, addr, is_shmem, file, nr, result), + TP_ARGS(mm, new_pfn, index, addr, is_shmem, file, nr, result), TP_STRUCT__entry( __field(struct mm_struct *, mm) __field(unsigned long, hpfn)
@@ -228,7 +228,7 @@ TRACE_EVENT(mm_khugepaged_collapse_file,TP_fast_assign ( __entry->mm = mm; - __entry->hpfn = new_folio ?folio_pfn (new_folio) :-1 ; + __entry->hpfn = new_pfn; __entry->index = index; __entry->addr = addr; __entry->is_shmem = is_shmem;
mm/khugepaged.c +3 -1
@@ -2260,6 +2260,7 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr,struct address_space *mapping = file->f_mapping;struct page *dst;struct folio *folio, *tmp, *new_folio; +unsigned long new_pfn =-1 ;pgoff_t index =0 , end = start +HPAGE_PMD_NR ;LIST_HEAD (pagelist);XA_STATE_ORDER (xas, &mapping->i_pages, start,HPAGE_PMD_ORDER );
@@ -2279,6 +2280,7 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr, result =alloc_charge_folio (&new_folio, mm, cc,HPAGE_PMD_ORDER );if (result !=SCAN_SUCCEED ) goto out; + new_pfn =folio_pfn (new_folio);mapping_set_update (&xas, mapping);
@@ -2682,7 +2684,7 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr,folio_put (new_folio); out:VM_BUG_ON (!list_empty (&pagelist)); -trace_mm_khugepaged_collapse_file (mm, new_folio, index, addr, is_shmem, file,HPAGE_PMD_NR , result); +trace_mm_khugepaged_collapse_file (mm, new_pfn, index, addr, is_shmem, file,HPAGE_PMD_NR , result);return result; }
Signature · 1 line
2.53.0
Andrew Morton
maintainerQuoted 9 lines · On Wed, 9 Sep 2026 10:58:00 +0800 Vernon Yang <[email protected]> wrote:
From: Vernon Yang <[email protected]>
The khugepaged tracepoints take a folio pointer and call
folio_pfn(), but by then the folio may no longer be valid: freed afterfolio_put(),folio_unlock()orpte_unmap_unlock(), or not a folio at all but an xarray-encoded swap entry. On classic SPARSEMEM, dereferencing it oopses khugepaged as soon as the trace event is enabled; on other memory models it merely prints a bogus pfn.
Quoted 4 lines
Pass the pfn to the tracepoints directly, captured while the folio is still pinned, closing the use-after-free windows in
mm_khugepaged_scan_file(),mm_khugepaged_scan_pmd()andmm_khugepaged_collapse_file().
Thanks, I've updated mm.git to this version.
Quoted 3 lines
V4 -> V5:
- Use a single trace statement.
- Collect Acked-by.
Here's how v5 altered mm.git:
mm/khugepaged.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-)
mm/khugepaged.c renamed from mm/khugepaged.c~b +4 -11
@@ -1790,13 +1790,10 @@ out_unmap: unmapped, cc, enabled_orders);/* mmap_lock was released above, set lock_dropped */ *lock_dropped = true; -trace_mm_khugepaged_scan_pmd (mm,-1 , referenced, none_or_zero, -SCAN_SUCCEED , unmapped); - }else { -out: -trace_mm_khugepaged_scan_pmd (mm, failed_pfn, referenced, - none_or_zero, result, unmapped); } +out: +trace_mm_khugepaged_scan_pmd (mm, failed_pfn, referenced, + none_or_zero, result, unmapped);return result; }
@@ -2789,13 +2786,9 @@ static enum scan_result collapse_scan_fi }else { result =collapse_file (mm, addr, file, start, cc); } -trace_mm_khugepaged_scan_file (mm,-1 , file, present, swap, -SCAN_SUCCEED ); - }else { -trace_mm_khugepaged_scan_file (mm, failed_pfn, file, present, - swap, result); } +trace_mm_khugepaged_scan_file (mm, failed_pfn, file, present, swap, result);return result; }
_