Skip to contentSkip to conversation

mm: khugepaged: fix tracepoint UAF

PATCH Superseded Vernon Yang 6 participants 22 messages Aug 15 – Aug 21

Subsystems (4)

v2 of 5 Superseded Latest is v5 →

lore
Share & tools
lore
#0 lore raw

v2 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().

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.

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                    | 23 ++++++++++++++++-------
 2 files changed, 25 insertions(+), 16 deletions(-)

base-commit: 075b74841bd0065a3bda3440873c747938e69b68 --
2.53.0

#1 lore raw

PATCH v2 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.

Fixes d41fd2016ed0 ("mm/khugepaged: add tracepoint to hpage_collapse_scan_file()")
---
 include/trace/events/huge_memory.h |  6 +++---
 mm/khugepaged.c                    | 14 +++++++++-----
 2 files changed, 12 insertions(+), 8 deletions(-)
include/trace/events/huge_memory.h +3 -3
index 291fae364c62..d3572d4ef453 100644
@@ -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 +9 -5
index 617bca76db49..e7830761d3a2 100644
@@ -2683,6 +2683,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 pfn;
 
 	present = 0;
 	swap = 0;
@@ -2720,27 +2721,23 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			 * PMD-sized THP implies that we can only try
 			 * retracting the PTE table.
 			 */
-			folio_put(folio);
 			break;
 		}
 
 		node = folio_nid(folio);
 		if (collapse_scan_abort(node, cc)) {
 			result = SCAN_SCAN_ABORT;
-			folio_put(folio);
 			break;
 		}
 		cc->node_load[node]++;
 
 		if (!folio_test_lru(folio)) {
 			result = SCAN_PAGE_LRU;
-			folio_put(folio);
 			break;
 		}
 
 		if (folio_expected_ref_count(folio) + 1 != folio_ref_count(folio)) {
 			result = SCAN_PAGE_COUNT;
-			folio_put(folio);
 			break;
 		}
 
@@ -2759,7 +2756,14 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			cond_resched_rcu();
 		}
 	}
+	if (!folio || xa_is_value(folio)) {
+		pfn = -1;
+	} else {
+		pfn = folio_pfn(folio);
+		folio_put(folio);
+	}
 	rcu_read_unlock();
+
 	if (result == SCAN_PTE_MAPPED_HUGEPAGE)
 		cc->progress++;
 	else
@@ -2774,7 +2778,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, pfn, file, present, swap, result);
 	return result;
 }
 
Signature · 1 line
2.53.0
#2 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

Quoted 6 lines · Vernon Yang

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.

Quoted 6 lines

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.

Hmm I seem to recall that we were no longer spporting !VMEMMAP SPARSEMEM, but maybe I was imagining that :)

But this needs fixing in general anyway.

Quoted 35 lines

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.

Fixes d41fd2016ed0 ("mm/khugepaged: add tracepoint to hpage_collapse_scan_file()")
---
 include/trace/events/huge_memory.h |  6 +++---
 mm/khugepaged.c                    | 14 +++++++++-----
 2 files changed, 12 insertions(+), 8 deletions(-)
include/trace/events/huge_memory.h +2 -2
index 291fae364c62..d3572d4ef453 100644
@@ -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)

include/trace/events/huge_memory.h TRACE_EVENT(mm_khugepaged_scan_file, · line 197

Quoted 5 lines
include/trace/events/huge_memory.h +1 -1
@@ -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;

I wonder how easy it is for people to interpret that this is a PFN of something only in the case of an early exit.

It's a bit of a mess that we're exposing internal implementation details like this, quite honestly.

I hope there is no expectation of this being there indefinitely.

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2686

Quoted 12 lines
include/trace/events/huge_memory.h +0 -0
 		__assign_str(filename);
 		__entry->present = present;
 		__entry->swap = swap;
mm/khugepaged.c +1 -0
index 617bca76db49..e7830761d3a2 100644
@@ -2683,6 +2683,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 pfn;

See below I think we should drop it.

Quoted 32 lines
mm/khugepaged.c +0 -4
 	present = 0;
 	swap = 0;
@@ -2720,27 +2721,23 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			 * PMD-sized THP implies that we can only try
 			 * retracting the PTE table.
 			 */
-			folio_put(folio);
 			break;
 		}
 
 		node = folio_nid(folio);
 		if (collapse_scan_abort(node, cc)) {
 			result = SCAN_SCAN_ABORT;
-			folio_put(folio);
 			break;
 		}
 		cc->node_load[node]++;
 
 		if (!folio_test_lru(folio)) {
 			result = SCAN_PAGE_LRU;
-			folio_put(folio);
 			break;
 		}
 
 		if (folio_expected_ref_count(folio) + 1 != folio_ref_count(folio)) {
 			result = SCAN_PAGE_COUNT;
-			folio_put(folio);
 			break;
 		}
 

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2764

Quoted 9 lines
mm/khugepaged.c +6 -0
@@ -2759,7 +2756,14 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			cond_resched_rcu();
 		}
 	}
+	if (!folio || xa_is_value(folio)) {
+		pfn = -1;
+	} else {
+		pfn = folio_pfn(folio);
+		folio_put(folio);
+	}

Hmm kinda hate this you're now mixing clean up logic with some trace logic.

You have to go check out xas_for_each() and see that xas_next_entry() sets folio to NULL at the end for the normal case to know this is OK and then figure out that this is just for the break cases.

That's not intuitive in already not intuitive code.

And this is a patch you're wanting to backport to fix a bug too... :)

I think you should keep the folio_put()'s where they are for now and limit this fix to the trace code please.

I think you should drop the pfn var altogether and do this far more simply, see below.

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2766

Quoted 2 lines
mm/khugepaged.c +1 -0
 	rcu_read_unlock();
+

Stray newline added, let's not do that for a backported fix please :)

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2781

Quoted 9 lines
mm/khugepaged.c +1 -1
 	if (result == SCAN_PTE_MAPPED_HUGEPAGE)
 		cc->progress++;
 	else
@@ -2774,7 +2778,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, pfn, file, present, swap, result);

I mean maybe better to just reduce this to something like:

	/* folio is NULL unless exited early. */
	trace_mm_khugepaged_scan_file(mm,
			(!folio || xa_is_value(folio)) ? -1 : folio_pfn(folio),
			file, present, swap, result);

As the least invasive way of fixing the trace + limiting the contageon?

Quoted 6 lines
mm/khugepaged.c +0 -1
 	return result;
 }
 
--

2.53.0

--
Cheers, Lorenzo

#3 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

Quoted 9 lines

I mean maybe better to just reduce this to something like:

	/* folio is NULL unless exited early. */
	trace_mm_khugepaged_scan_file(mm,
			(!folio || xa_is_value(folio)) ? -1 : folio_pfn(folio),
			file, present, swap, result);

As the least invasive way of fixing the trace + limiting the contageon?

I raised that using a folio after dropping relevant refs+locks is in general an anti-pattern (IOW wrong if folios can just get split afterwards and are suddenly no longer folios).

I'd like us to avoid that where possible (and suspect Willy will thank us later ;) )

Isn't there some minimal way to just calculate the pfn once we obtain a folio? I was assuming that there should be an easier way to do that.

Signature · 3 lines
Cheers,

David
#4 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

Quoted 10 lines · David Hildenbrand (Arm)
Quoted 9 lines

I mean maybe better to just reduce this to something like:

	/* folio is NULL unless exited early. */
	trace_mm_khugepaged_scan_file(mm,
			(!folio || xa_is_value(folio)) ? -1 : folio_pfn(folio),
			file, present, swap, result);

As the least invasive way of fixing the trace + limiting the contageon?

Quoted 8 lines

I raised that using a folio after dropping relevant refs+locks is in general an anti-pattern (IOW wrong if folios can just get split afterwards and are suddenly no longer folios).

I'd like us to avoid that where possible (and suspect Willy will thank us later ;) )

Isn't there some minimal way to just calculate the pfn once we obtain a folio? I was assuming that there should be an easier way to do that.

See my reply-to-self. My initial reply was wrong because parisc + mips do actually do classic sparsemem (sigh).

I suggest what you're saying here basically.

(Moving folio_put()'s for the break cases out of the loop is nasty)

Quoted 5 lines

--
Cheers,

David

--
Cheers, Lorenzo

#5 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

In reply to Lorenzo Stoakes (ARM)
Quoted 14 lines · Lorenzo Stoakes (ARM)
Quoted 6 lines · Vernon Yang

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.

Quoted 6 lines

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.

Quoted 2 lines

Hmm I seem to recall that we were no longer spporting !VMEMMAP SPARSEMEM, but maybe I was imagining that :)

OK seems some museum piece architectures are causing us issues again, fun times (parisc and some mips).

Quoted 52 lines

But this needs fixing in general anyway.

Quoted 35 lines

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.

Fixes d41fd2016ed0 ("mm/khugepaged: add tracepoint to hpage_collapse_scan_file()")
---
 include/trace/events/huge_memory.h |  6 +++---
 mm/khugepaged.c                    | 14 +++++++++-----
 2 files changed, 12 insertions(+), 8 deletions(-)
include/trace/events/huge_memory.h +2 -2
index 291fae364c62..d3572d4ef453 100644
@@ -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)

include/trace/events/huge_memory.h TRACE_EVENT(mm_khugepaged_scan_file, · line 197

Quoted 5 lines
include/trace/events/huge_memory.h +1 -1
@@ -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;

I wonder how easy it is for people to interpret that this is a PFN of something only in the case of an early exit.

It's a bit of a mess that we're exposing internal implementation details like this, quite honestly.

I hope there is no expectation of this being there indefinitely.

Quoted 14 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2686

Quoted 12 lines
include/trace/events/huge_memory.h +0 -0
 		__assign_str(filename);
 		__entry->present = present;
 		__entry->swap = swap;
mm/khugepaged.c +1 -0
index 617bca76db49..e7830761d3a2 100644
@@ -2683,6 +2683,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 pfn;

See below I think we should drop it.

OK we can't because of museum piece architectures that will actually deref the folio to get the PFN...

I'd startr by initialising this to -1 then since I guess in theory it's possible xas_for_each() could just not do anything is it? (folio is being set to NULL so that suggests so).

Quoted 9 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2723

Quoted 9 lines
mm/khugepaged.c +0 -1
 	present = 0;
 	swap = 0;
@@ -2720,27 +2721,23 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			 * PMD-sized THP implies that we can only try
 			 * retracting the PTE table.
 			 */
-			folio_put(folio);

I still hate the idea of moving these outside of the loop, it's just asking for trouble.

So I guess instead:

	xas_for_each(&xas, folio, start + HPAGE_PMD_NR - 1) {
		pfn = -1;
		...
		if (xa_is_value(folio)) {
			...
		}
		pfn = folio_pfn(folio);
		...
	}
	...
	trace_mm_khugepaged_scan_file(mm, pfn, file, present, swap, result);

That way you avoid the horror or moving the folio put somewhere deeply unintuitive and inconsistent, and maintain pfn lifetime for some arches nobody uses.

Quoted 67 lines
Quoted 24 lines
mm/khugepaged.c +0 -3
 			break;
 		}
 
 		node = folio_nid(folio);
 		if (collapse_scan_abort(node, cc)) {
 			result = SCAN_SCAN_ABORT;
-			folio_put(folio);
 			break;
 		}
 		cc->node_load[node]++;
 
 		if (!folio_test_lru(folio)) {
 			result = SCAN_PAGE_LRU;
-			folio_put(folio);
 			break;
 		}
 
 		if (folio_expected_ref_count(folio) + 1 != folio_ref_count(folio)) {
 			result = SCAN_PAGE_COUNT;
-			folio_put(folio);
 			break;
 		}
 

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2764

Quoted 9 lines
mm/khugepaged.c +6 -0
@@ -2759,7 +2756,14 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			cond_resched_rcu();
 		}
 	}
+	if (!folio || xa_is_value(folio)) {
+		pfn = -1;
+	} else {
+		pfn = folio_pfn(folio);
+		folio_put(folio);
+	}

Hmm kinda hate this you're now mixing clean up logic with some trace logic.

You have to go check out xas_for_each() and see that xas_next_entry() sets folio to NULL at the end for the normal case to know this is OK and then figure out that this is just for the break cases.

That's not intuitive in already not intuitive code.

And this is a patch you're wanting to backport to fix a bug too... :)

I think you should keep the folio_put()'s where they are for now and limit this fix to the trace code please.

I think you should drop the pfn var altogether and do this far more simply, see below.

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2766

Quoted 2 lines
mm/khugepaged.c +1 -0
 	rcu_read_unlock();
+

Stray newline added, let's not do that for a backported fix please :)

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2781

Quoted 9 lines
mm/khugepaged.c +1 -1
 	if (result == SCAN_PTE_MAPPED_HUGEPAGE)
 		cc->progress++;
 	else
@@ -2774,7 +2778,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, pfn, file, present, swap, result);

I mean maybe better to just reduce this to something like:

Quoted 6 lines
	/* folio is NULL unless exited early. */
	trace_mm_khugepaged_scan_file(mm,
			(!folio || xa_is_value(folio)) ? -1 : folio_pfn(folio),
			file, present, swap, result);

As the least invasive way of fixing the trace + limiting the contageon?

Yeah as above this isn't right.

Quoted 10 lines
Quoted 7 lines
mm/khugepaged.c +0 -1
 	return result;
 }
 
--

2.53.0

--
Cheers, Lorenzo

--
Cheers, Lorenzo

#6 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

Quoted 121 lines · Lorenzo Stoakes (ARM)
Quoted 14 lines · Lorenzo Stoakes (ARM)
Quoted 6 lines · Vernon Yang

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.

Quoted 6 lines

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.

Quoted 2 lines

Hmm I seem to recall that we were no longer spporting !VMEMMAP SPARSEMEM, but maybe I was imagining that :)

OK seems some museum piece architectures are causing us issues again, fun times (parisc and some mips).

Quoted 52 lines

But this needs fixing in general anyway.

Quoted 35 lines

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.

Fixes d41fd2016ed0 ("mm/khugepaged: add tracepoint to hpage_collapse_scan_file()")
---
 include/trace/events/huge_memory.h |  6 +++---
 mm/khugepaged.c                    | 14 +++++++++-----
 2 files changed, 12 insertions(+), 8 deletions(-)
include/trace/events/huge_memory.h +2 -2
index 291fae364c62..d3572d4ef453 100644
@@ -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)

include/trace/events/huge_memory.h TRACE_EVENT(mm_khugepaged_scan_file, · line 197

Quoted 5 lines
include/trace/events/huge_memory.h +1 -1
@@ -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;

I wonder how easy it is for people to interpret that this is a PFN of something only in the case of an early exit.

It's a bit of a mess that we're exposing internal implementation details like this, quite honestly.

I hope there is no expectation of this being there indefinitely.

Quoted 14 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2686

Quoted 12 lines
include/trace/events/huge_memory.h +0 -0
 		__assign_str(filename);
 		__entry->present = present;
 		__entry->swap = swap;
mm/khugepaged.c +1 -0
index 617bca76db49..e7830761d3a2 100644
@@ -2683,6 +2683,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 pfn;

See below I think we should drop it.

OK we can't because of museum piece architectures that will actually deref the folio to get the PFN...

I'd startr by initialising this to -1 then since I guess in theory it's possible xas_for_each() could just not do anything is it? (folio is being set to NULL so that suggests so).

Quoted 9 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2723

Quoted 9 lines
mm/khugepaged.c +0 -1
 	present = 0;
 	swap = 0;
@@ -2720,27 +2721,23 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			 * PMD-sized THP implies that we can only try
 			 * retracting the PTE table.
 			 */
-			folio_put(folio);

I still hate the idea of moving these outside of the loop, it's just asking for trouble.

So I guess instead:

	xas_for_each(&xas, folio, start + HPAGE_PMD_NR - 1) {
		pfn = -1;
		...
		if (xa_is_value(folio)) {
			...
		}
		pfn = folio_pfn(folio);
		...
	}
	...
	trace_mm_khugepaged_scan_file(mm, pfn, file, present, swap, result);
Quoted 3 lines

That way you avoid the horror or moving the folio put somewhere deeply unintuitive and inconsistent, and maintain pfn lifetime for some arches nobody uses.

Thank you for the reference solution. After studying the overall process, I found that the above solution still has two issues:

- When !folio_try_get(folio) or folio != xas_reload(&xas), if the next xas_for_each() iteration happens to terminate, pfn will be incorrect, which is inconsistent with previous versions.
- when the xas_for_each() iteration to terminate and the folio operation preceding is normal. but pfn will be incorrect, also same issue.

To keep it simple, I modified it as follows. Is this OK?

mm/khugepaged.c +4 -1
index 79effd3f3da4..00337405c0e0 100644
@@ -2689,6 +2689,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 pfn;
 
 	present = 0;
 	swap = 0;
@@ -2719,6 +2720,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			continue;
 		}
 
+		pfn = folio_pfn(folio);
 		if (is_pmd_order(folio_order(folio))) {
 			result = SCAN_PTE_MAPPED_HUGEPAGE;
 			/*
@@ -2779,7 +2781,8 @@ 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, (!folio || xa_is_value(folio)) ? -1 : pfn,
+				      file, present, swap, result);
 	return result;
 }
 

--
Cheers,
Vernon

Quoted 91 lines
Quoted 68 lines
Quoted 25 lines
mm/khugepaged.c +0 -3
 			break;
 		}
 
 		node = folio_nid(folio);
 		if (collapse_scan_abort(node, cc)) {
 			result = SCAN_SCAN_ABORT;
-			folio_put(folio);
 			break;
 		}
 		cc->node_load[node]++;
 
 		if (!folio_test_lru(folio)) {
 			result = SCAN_PAGE_LRU;
-			folio_put(folio);
 			break;
 		}
 
 		if (folio_expected_ref_count(folio) + 1 != folio_ref_count(folio)) {
 			result = SCAN_PAGE_COUNT;
-			folio_put(folio);
 			break;
 		}
 

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2764

Quoted 9 lines
mm/khugepaged.c +6 -0
@@ -2759,7 +2756,14 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			cond_resched_rcu();
 		}
 	}
+	if (!folio || xa_is_value(folio)) {
+		pfn = -1;
+	} else {
+		pfn = folio_pfn(folio);
+		folio_put(folio);
+	}

Hmm kinda hate this you're now mixing clean up logic with some trace logic.

You have to go check out xas_for_each() and see that xas_next_entry() sets folio to NULL at the end for the normal case to know this is OK and then figure out that this is just for the break cases.

That's not intuitive in already not intuitive code.

And this is a patch you're wanting to backport to fix a bug too... :)

I think you should keep the folio_put()'s where they are for now and limit this fix to the trace code please.

I think you should drop the pfn var altogether and do this far more simply, see below.

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2766

Quoted 2 lines
mm/khugepaged.c +1 -0
 	rcu_read_unlock();
+

Stray newline added, let's not do that for a backported fix please :)

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2781

Quoted 9 lines
mm/khugepaged.c +1 -1
 	if (result == SCAN_PTE_MAPPED_HUGEPAGE)
 		cc->progress++;
 	else
@@ -2774,7 +2778,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, pfn, file, present, swap, result);

I mean maybe better to just reduce this to something like:

Quoted 6 lines
	/* folio is NULL unless exited early. */
	trace_mm_khugepaged_scan_file(mm,
			(!folio || xa_is_value(folio)) ? -1 : folio_pfn(folio),
			file, present, swap, result);

As the least invasive way of fixing the trace + limiting the contageon?

Yeah as above this isn't right.

Quoted 10 lines
Quoted 7 lines
mm/khugepaged.c +0 -1
 	return result;
 }
 
--

2.53.0

--
Cheers, Lorenzo

--
Cheers, Lorenzo

#7 lore raw

Re: [PATCH v2 1/3] mm: khugepaged: fix swap entry value to folio_pfn()

Quoted 126 lines · Vernon Yang
Quoted 121 lines · Lorenzo Stoakes (ARM)
Quoted 14 lines · Lorenzo Stoakes (ARM)
Quoted 6 lines · Vernon Yang

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.

Quoted 6 lines

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.

Quoted 2 lines

Hmm I seem to recall that we were no longer spporting !VMEMMAP SPARSEMEM, but maybe I was imagining that :)

OK seems some museum piece architectures are causing us issues again, fun times (parisc and some mips).

Quoted 52 lines

But this needs fixing in general anyway.

Quoted 35 lines

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.

Fixes d41fd2016ed0 ("mm/khugepaged: add tracepoint to hpage_collapse_scan_file()")
---
 include/trace/events/huge_memory.h |  6 +++---
 mm/khugepaged.c                    | 14 +++++++++-----
 2 files changed, 12 insertions(+), 8 deletions(-)
include/trace/events/huge_memory.h +2 -2
index 291fae364c62..d3572d4ef453 100644
@@ -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)

include/trace/events/huge_memory.h TRACE_EVENT(mm_khugepaged_scan_file, · line 197

Quoted 5 lines
include/trace/events/huge_memory.h +1 -1
@@ -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;

I wonder how easy it is for people to interpret that this is a PFN of something only in the case of an early exit.

It's a bit of a mess that we're exposing internal implementation details like this, quite honestly.

I hope there is no expectation of this being there indefinitely.

Quoted 14 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2686

Quoted 12 lines
include/trace/events/huge_memory.h +0 -0
 		__assign_str(filename);
 		__entry->present = present;
 		__entry->swap = swap;
mm/khugepaged.c +1 -0
index 617bca76db49..e7830761d3a2 100644
@@ -2683,6 +2683,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 pfn;

See below I think we should drop it.

OK we can't because of museum piece architectures that will actually deref the folio to get the PFN...

I'd startr by initialising this to -1 then since I guess in theory it's possible xas_for_each() could just not do anything is it? (folio is being set to NULL so that suggests so).

Quoted 9 lines

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2723

Quoted 9 lines
mm/khugepaged.c +0 -1
 	present = 0;
 	swap = 0;
@@ -2720,27 +2721,23 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			 * PMD-sized THP implies that we can only try
 			 * retracting the PTE table.
 			 */
-			folio_put(folio);

I still hate the idea of moving these outside of the loop, it's just asking for trouble.

So I guess instead:

	xas_for_each(&xas, folio, start + HPAGE_PMD_NR - 1) {
		pfn = -1;
		...
		if (xa_is_value(folio)) {
			...
		}
		pfn = folio_pfn(folio);
		...
	}
	...
	trace_mm_khugepaged_scan_file(mm, pfn, file, present, swap, result);
Quoted 3 lines

That way you avoid the horror or moving the folio put somewhere deeply unintuitive and inconsistent, and maintain pfn lifetime for some arches nobody uses.

Quoted 6 lines

Thank you for the reference solution. After studying the overall process, I found that the above solution still has two issues:

- When !folio_try_get(folio) or folio != xas_reload(&xas), if the next xas_for_each() iteration happens to terminate, pfn will be incorrect, which is inconsistent with previous versions.

Ah you're right of course!

Quoted 3 lines

- when the xas_for_each() iteration to terminate and the folio operation preceding is normal. but pfn will be incorrect, also same issue.

Yeah... :)

Quoted 24 lines

To keep it simple, I modified it as follows. Is this OK?

mm/khugepaged.c +2 -0
index 79effd3f3da4..00337405c0e0 100644
@@ -2689,6 +2689,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 pfn;
 
 	present = 0;
 	swap = 0;
@@ -2719,6 +2720,7 @@ static enum scan_result collapse_scan_file(struct mm_struct *mm,
 			continue;
 		}
 
+		pfn = folio_pfn(folio);
 		if (is_pmd_order(folio_order(folio))) {
 			result = SCAN_PTE_MAPPED_HUGEPAGE;
 			/*

mm/khugepaged.c static enum scan_result collapse_scan_file(struct mm_struct *mm, · line 2787

Quoted 8 lines
mm/khugepaged.c +2 -1
@@ -2779,7 +2781,8 @@ 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, (!folio || xa_is_value(folio)) ? -1 : pfn,
+				      file, present, swap, result);
 	return result;
 }

OK looks reasonable, thanks! :)

Quoted 5 lines
mm/khugepaged.c +0 -1
--

Cheers,
Vernon

#8 lore raw

PATCH v2 2/3 mm: khugepaged: fix folio is used after pte_unmap_unlock()

In reply to Vernon Yang

From: 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.

Fixes 7d2eba0557c1 ("mm: add tracepoint for scanning pages")
---
 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
index d3572d4ef453..5dc71d292f47 100644
@@ -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 +3 -1
index e7830761d3a2..7c8c48577408 100644
@@ -1603,6 +1603,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 pfn = -1;
 	unsigned long addr;
 	unsigned long enabled_orders;
 	spinlock_t *ptl;
@@ -1778,6 +1779,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
 		result = SCAN_SUCCEED;
 	}
 out_unmap:
+	pfn = folio ? folio_pfn(folio) : -1;
 	pte_unmap_unlock(pte, ptl);
 	if (result == SCAN_SUCCEED) {
 		/* collapse_huge_page expects the lock to be dropped before calling */
@@ -1788,7 +1790,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, pfn, referenced,
 				     none_or_zero, result, unmapped);
 	return result;
 }
Signature · 1 line
2.53.0
#9 lore raw

Re: [PATCH v2 2/3] mm: khugepaged: fix folio is used after pte_unmap_unlock()

Quoted 64 lines · Vernon Yang

From: 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.

Fixes 7d2eba0557c1 ("mm: add tracepoint for scanning pages")
---
 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
index d3572d4ef453..5dc71d292f47 100644
@@ -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 +2 -0
index e7830761d3a2..7c8c48577408 100644
@@ -1603,6 +1603,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 pfn = -1;
 	unsigned long addr;
 	unsigned long enabled_orders;
 	spinlock_t *ptl;
@@ -1778,6 +1779,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
 		result = SCAN_SUCCEED;
 	}
 out_unmap:
+	pfn = folio ? folio_pfn(folio) : -1;
 	pte_unmap_unlock(pte, ptl);
 	if (result == SCAN_SUCCEED) {
 		/* collapse_huge_page expects the lock to be dropped before calling */

mm/khugepaged.c static enum scan_result collapse_scan_pmd(struct mm_struct *mm, · line 1793

Quoted 5 lines
mm/khugepaged.c +1 -1
@@ -1788,7 +1790,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, pfn, referenced,

Same comment as 1/3 I don't see why we should be storing a pfn value used nowhere else just for tracing.

Quoted 6 lines
mm/khugepaged.c +0 -1
 				     none_or_zero, result, unmapped);
 	return result;
 }
--

2.53.0

--
Cheers, Lorenzo

#10 lore raw

Re: [PATCH v2 2/3] mm: khugepaged: fix folio is used after pte_unmap_unlock()

Quoted 65 lines · Lorenzo Stoakes (ARM)
Quoted 64 lines · Vernon Yang

From: 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.

Fixes 7d2eba0557c1 ("mm: add tracepoint for scanning pages")
---
 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
index d3572d4ef453..5dc71d292f47 100644
@@ -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 +2 -0
index e7830761d3a2..7c8c48577408 100644
@@ -1603,6 +1603,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 pfn = -1;
 	unsigned long addr;
 	unsigned long enabled_orders;
 	spinlock_t *ptl;
@@ -1778,6 +1779,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
 		result = SCAN_SUCCEED;
 	}
 out_unmap:
+	pfn = folio ? folio_pfn(folio) : -1;
 	pte_unmap_unlock(pte, ptl);
 	if (result == SCAN_SUCCEED) {
 		/* collapse_huge_page expects the lock to be dropped before calling */
@@ -1788,7 +1790,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
Quoted 8 lines

mm/khugepaged.c static enum scan_result collapse_scan_pmd(struct mm_struct *mm, · line 1793

Quoted 5 lines
mm/khugepaged.c +1 -1
 		*lock_dropped = true;
 	}
 out:
-	trace_mm_khugepaged_scan_pmd(mm, folio, referenced,
+	trace_mm_khugepaged_scan_pmd(mm, pfn, referenced,

Same comment as 1/3 I don't see why we should be storing a pfn value used nowhere else just for tracing.

I prefer it that way. Even if just for tracing. :)

Signature · 3 lines
Cheers,

David
#11 lore raw

Re: [PATCH v2 2/3] mm: khugepaged: fix folio is used after pte_unmap_unlock()

Quoted 5 lines · David Hildenbrand (Arm)
Quoted 2 lines · Lorenzo Stoakes (ARM)

Same comment as 1/3 I don't see why we should be storing a pfn value used nowhere else just for tracing.

I prefer it that way. Even if just for tracing. :)

This is moot because you have to store the PFN for lifetime for museum-piece architectures.

But anyway FWIW:

Yes I agree accessing a now-invalid folio isn't great, but the reason I dislike this pattern is that you're tracking this state in a different way that now has maintenance overhead (if anybody reassigns folio or does weird control flow etc. they must now update 2 things and etc.)

For the sake of CONFIG_SPARSEMEM and tracing for a value that is not at all intuitive as to what it means, that's not great.

Let me go back and review this again accounting for the lifetime issue that I missed first time.

Quoted 5 lines

--
Cheers,

David

--
Cheers, Lorenzo

#12 lore raw

Re: [PATCH v2 2/3] mm: khugepaged: fix folio is used after pte_unmap_unlock()

In reply to Lorenzo Stoakes (ARM)
Quoted 53 lines · Lorenzo Stoakes (ARM)
Quoted 52 lines · Vernon Yang

From: 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.

Fixes 7d2eba0557c1 ("mm: add tracepoint for scanning pages")
---
 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
index d3572d4ef453..5dc71d292f47 100644
@@ -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 +1 -0
index e7830761d3a2..7c8c48577408 100644
@@ -1603,6 +1603,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 pfn = -1;
Quoted 8 lines

mm/khugepaged.c static enum scan_result collapse_scan_pmd(struct mm_struct *mm, · line 1782

Quoted 8 lines
mm/khugepaged.c +1 -0
 	unsigned long addr;
 	unsigned long enabled_orders;
 	spinlock_t *ptl;
@@ -1778,6 +1779,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
 		result = SCAN_SUCCEED;
 	}
 out_unmap:
+	pfn = folio ? folio_pfn(folio) : -1;

You already defaulted the value to -1, better as:

	if (folio)
		pfn = folio_pfn(folio);
Quoted 4 lines
Quoted 4 lines
mm/khugepaged.c +0 -0
 	pte_unmap_unlock(pte, ptl);
 	if (result == SCAN_SUCCEED) {
 		/* collapse_huge_page expects the lock to be dropped before calling */
@@ -1788,7 +1790,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
Quoted 8 lines

mm/khugepaged.c static enum scan_result collapse_scan_pmd(struct mm_struct *mm, · line 1793

Quoted 5 lines
mm/khugepaged.c +1 -1
 		*lock_dropped = true;
 	}
 out:
-	trace_mm_khugepaged_scan_pmd(mm, folio, referenced,
+	trace_mm_khugepaged_scan_pmd(mm, pfn, referenced,

Same comment as 1/3 I don't see why we should be storing a pfn value used nowhere else just for tracing.

Disregard, I didn't account for lifetime.

Quoted 10 lines
Quoted 7 lines
mm/khugepaged.c +0 -1
 				     none_or_zero, result, unmapped);
 	return result;
 }
--

2.53.0

--
Cheers, Lorenzo

--
Cheers, Lorenzo

#13 lore raw

Re: [PATCH v2 2/3] mm: khugepaged: fix folio is used after pte_unmap_unlock()

Quoted 65 lines · Lorenzo Stoakes
Quoted 53 lines · Lorenzo Stoakes (ARM)
Quoted 52 lines · Vernon Yang

From: 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.

Fixes 7d2eba0557c1 ("mm: add tracepoint for scanning pages")
---
 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
index d3572d4ef453..5dc71d292f47 100644
@@ -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 +1 -0
index e7830761d3a2..7c8c48577408 100644
@@ -1603,6 +1603,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 pfn = -1;
Quoted 8 lines

mm/khugepaged.c static enum scan_result collapse_scan_pmd(struct mm_struct *mm, · line 1782

Quoted 8 lines
mm/khugepaged.c +1 -0
 	unsigned long addr;
 	unsigned long enabled_orders;
 	spinlock_t *ptl;
@@ -1778,6 +1779,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
 		result = SCAN_SUCCEED;
 	}
 out_unmap:
+	pfn = folio ? folio_pfn(folio) : -1;

You already defaulted the value to -1, better as:

Quoted 3 lines
	if (folio)
		pfn = folio_pfn(folio);

LGTM, Thanks!

--
Cheers,
Vernon

#14 lore raw

PATCH v2 3/3 mm: khugepaged: fix folio is used after folio_put/unlock()

In reply to Vernon Yang

From: 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.

Fixes 4c9473e87e75 ("mm/khugepaged: add tracepoint to collapse_file()")
---
 include/trace/events/huge_memory.h | 6 +++---
 mm/khugepaged.c                    | 5 ++++-
 2 files changed, 7 insertions(+), 4 deletions(-)
include/trace/events/huge_memory.h +3 -3
index 5dc71d292f47..1b301d4e2dba 100644
@@ -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 +4 -1
index 7c8c48577408..7df0fd2d2fdc 100644
@@ -2247,6 +2247,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);
@@ -2626,6 +2627,7 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr,
 	retract_page_tables(mapping, start);
 	if (cc && !cc->is_khugepaged)
 		result = SCAN_PTE_MAPPED_HUGEPAGE;
+	new_pfn = folio_pfn(new_folio);
 	folio_unlock(new_folio);
 
 	/*
@@ -2664,12 +2666,13 @@ static enum scan_result collapse_file(struct mm_struct *mm, unsigned long addr,
 	}
 
 	new_folio->mapping = NULL;
+	new_pfn = folio_pfn(new_folio);
 
 	folio_unlock(new_folio);
 	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
reviewer Tested-by on the v2 series
#15 lore raw
In reply to Vernon Yang
Quoted 9 lines · Vernon Yang

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.

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() and mm_khugepaged_collapse_file().

Well spotted!

Gave the series a run on x86_64 (KVM), all good (only classic SPARSEMEM untested) :)

reviewer
#16 lore raw

+Cc Baolin

Quoted 16 lines · Lance Yang
Quoted 9 lines · Vernon Yang

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.

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() and mm_khugepaged_collapse_file().

Quoted 4 lines

Well spotted!

Gave the series a run on x86_64 (KVM), all good (only classic SPARSEMEM untested) :)

Hmm ... stumbled over something else while testing this ...

With tmpfs mounted huge=advise, one MADV_HUGEPAGE isn't enough to get an unregistered mm onto khugepaged's list. Do it twice, and khugepaged starts scanning right away.

The pending flags make it into khugepaged just fine:

int hugepage_madvise(struct vm_area_struct *vma,
vm_flags_t *vm_flags, int advice)

{
	switch (advice) {
	case MADV_HUGEPAGE:
		*vm_flags &= ~VM_NOHUGEPAGE;
		*vm_flags |= VM_HUGEPAGE;
...
		khugepaged_enter_vma(vma, *vm_flags);
		break;
...
	}

	return 0;
}

and survive the common eligibility check:

unsigned long __thp_vma_allowable_orders(struct vm_area_struct *vma,
vm_flags_t vm_flags, enum tva_type type, unsigned long orders)
{
...

	/*
	 * Enabled via shmem mount options or sysfs settings.
	 * Must be done before hugepage flags check since shmem has its
	 * own flags.
	 */
	if (!in_pf && shmem_file(vma->vm_file))
		return orders & shmem_allowable_huge_orders(file_inode(vma->vm_file),
						   vma, vma_start_pgoff(vma), 0,
						   forced_collapse);

...
}

But then the shmem helper reads them back from the VMA:

unsigned long shmem_allowable_huge_orders(struct inode *inode,
struct vm_area_struct *vma, pgoff_t index, loff_t write_end, bool shmem_huge_force)
{
...

	vm_flags_t vm_flags = vma ? vma->vm_flags : 0;

...
}

At that point vma->vm_flags still has the old value, so huge=advise quietly gives us no allowable order.

First madvise still succeeds, of course. The second one works because it finds VM_HUGEPAGE already installed by the first call.

Looked at history too ... we've been here before. 2cf1338454a8 ("mm: fix khugepaged with shmem_enabled=advise") fixed this exact ordering bug and tagged cd89fb065099 as the culprit. Then 6beeab870e70 was meant to be "No functional changes", but moving shmem_huge_global_enabled() into shmem_allowable_huge_orders() seems to have wired the stale vma->vm_flags read back in.

So AFAICT, this regressed in v6.12 with 6beeab870e70.

@Baolin, does that ring a bell? Any reason shmem_allowable_huge_orders() can't just take the pending vm_flags as well?

Cheers, Lance

reviewer
#17 lore raw

Hi Lance,

Quoted 92 lines · Lance Yang

+Cc Baolin

Quoted 16 lines · Lance Yang
Quoted 9 lines · Vernon Yang

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.

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() and mm_khugepaged_collapse_file().

Quoted 4 lines

Well spotted!

Gave the series a run on x86_64 (KVM), all good (only classic SPARSEMEM untested) :)

Hmm ... stumbled over something else while testing this ...

With tmpfs mounted huge=advise, one MADV_HUGEPAGE isn't enough to get an unregistered mm onto khugepaged's list. Do it twice, and khugepaged starts scanning right away.

The pending flags make it into khugepaged just fine:

int hugepage_madvise(struct vm_area_struct *vma,
vm_flags_t *vm_flags, int advice)

{
	switch (advice) {
	case MADV_HUGEPAGE:
		*vm_flags &= ~VM_NOHUGEPAGE;
		*vm_flags |= VM_HUGEPAGE;
...
		khugepaged_enter_vma(vma, *vm_flags);
		break;
...
	}

	return 0;
}

and survive the common eligibility check:

unsigned long __thp_vma_allowable_orders(struct vm_area_struct *vma,
vm_flags_t vm_flags, enum tva_type type, unsigned long orders)
{
...

	/*
	 * Enabled via shmem mount options or sysfs settings.
	 * Must be done before hugepage flags check since shmem has its
	 * own flags.
	 */
	if (!in_pf && shmem_file(vma->vm_file))
		return orders & shmem_allowable_huge_orders(file_inode(vma->vm_file),
						   vma, vma_start_pgoff(vma), 0,
						   forced_collapse);

...
}

But then the shmem helper reads them back from the VMA:

unsigned long shmem_allowable_huge_orders(struct inode *inode,
struct vm_area_struct *vma, pgoff_t index, loff_t write_end, bool shmem_huge_force)
{
...

	vm_flags_t vm_flags = vma ? vma->vm_flags : 0;

...
}

At that point vma->vm_flags still has the old value, so huge=advise quietly gives us no allowable order.

First madvise still succeeds, of course. The second one works because it finds VM_HUGEPAGE already installed by the first call.

Looked at history too ... we've been here before. 2cf1338454a8 ("mm: fix khugepaged with shmem_enabled=advise") fixed this exact ordering bug and tagged cd89fb065099 as the culprit. Then 6beeab870e70 was meant to be "No functional changes", but moving shmem_huge_global_enabled() into shmem_allowable_huge_orders() seems to have wired the stale vma->vm_flags read back in.

Quoted 4 lines

So AFAICT, this regressed in v6.12 with 6beeab870e70.

@Baolin, does that ring a bell? Any reason shmem_allowable_huge_orders() can't just take the pending vm_flags as well?

Good catch. Sorry for my mistake. Would you like to send a fix? Otherwise, I can fix it. Thanks for your report and analysis.

reviewer
#18 lore raw
Quoted 8 lines · Baolin Wang

Hi Lance,

Quoted 5 lines · Lance Yang

+Cc Baolin

Quoted 2 lines · Lance Yang

On Sat, Aug 15, 2026 at 01:19:21PM +0800, Vernon Yang wrote:

[...]

Quoted 1 line

Otherwise, I can fix it. Thanks for your report and analysis.

Yeah, please go ahead :) Thanks, Baolin!

reviewer
#19 lore raw
Quoted 8 lines · Lance Yang
Quoted 5 lines · Baolin Wang

Hi Lance,

Quoted 2 lines · Lance Yang

+Cc Baolin

Quoted 7 lines
Quoted 3 lines
Quoted 3 lines · Lance Yang
Quoted 2 lines · Vernon Yang

[...]

Quoted 1 line

Otherwise, I can fix it. Thanks for your report and analysis.

Yeah, please go ahead :) Thanks, Baolin!

FYI: I've posted the fix and verified it resolves the issue from my testing.

https://lore.kernel.org/all/ed34ca03ae7d65e89467fb87bc961f5497049c00.1786948410.git.baolin.wang@linux.alibaba.com/

maintainer
#20 lore raw
In reply to Lance Yang
Quoted 11 lines · On Sun, 16 Aug 2026 02:16:32 +0800 Lance Yang <[email protected]> wrote:
Quoted 9 lines
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() and mm_khugepaged_collapse_file().

Well spotted!

Gave the series a run on x86_64 (KVM), all good (only classic SPARSEMEM untested) :)

Hmm ... stumbled over something else while testing this ...

Sashiko might have found a couple of pre-existing things.

	https://sashiko.dev/#/patchset/[email protected]

Vernon, you may decide that the second one is on-topic for this patchset?

#21 lore raw
Quoted 12 lines · Andrew Morton
Quoted 10 lines · On Sun, 16 Aug 2026 02:16:32 +0800 Lance Yang <[email protected]> wrote:
Quoted 9 lines
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() and mm_khugepaged_collapse_file().

Well spotted!

Gave the series a run on x86_64 (KVM), all good (only classic SPARSEMEM untested) :)

Quoted 6 lines
Quoted 1 line

Hmm ... stumbled over something else while testing this ...

Sashiko might have found a couple of pre-existing things.

	https://sashiko.dev/#/patchset/[email protected]

Vernon, you may decide that the second one is on-topic for this patchset?

Hi, Andrew, the second one already is on-topic for this patchset, detail look for the PATCH#3 please.

--
Cheers,
Vernon