[RFC PATCH 56/57] selftests/mm: cover the scaled max_ptes_shared limit

Kiryl Shutsemau <[email protected]>
Newsgroups org.kernel.vger.linux-kselftest,org.kernel.vger.bpf,org.kernel.vger.linux-kernel,org.kernel.vger.linux-trace-kernel,org.kvack.linux-mm
Message-ID <[email protected]>
From: "Kiryl Shutsemau (Meta)" <[email protected]>

max_ptes_shared is written in PTEs of a whole PMD, but a range smaller
than a PMD is scanned in full and judged by the same setting.  Compared
raw the limit is then unreachable: on arm64 with 64K base pages a 2M range
holds 32 PTEs and can never exceed a 4096-PTE budget.  So it is scaled to
what was actually scanned, and what decides is the shared fraction rather
than the count.

Cover both directions in one sub-PMD range, with a forked child holding
the sources shared.  One PTE past the scaled budget: nothing in the range
collapses.  Break CoW on one more page, bringing it inside: it collapses.

The counts come from the current max_ptes_shared rather than being
hardcoded, so the case follows the setting and the order it runs at.

Without the scaling the first half passes wrongly: a few dozen shared PTEs
never reach a limit expressed in PMD units, so the range collapses when it
should not.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 tools/testing/selftests/mm/khugepaged.c | 85 +++++++++++++++++++++++++
 1 file changed, 85 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index bd684cd25fed..dde24756a3a2 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1791,6 +1791,90 @@ static void collapse_order_pinned_window(struct collapse_context *c,
 	ksft_test_result_report(exit_status, "%s\n", __func__);
 }
 
+/*
+ * max_ptes_shared counts PTEs of a whole PMD, but a VMA smaller than one is
+ * scanned in full and judged against the same setting, so the limit is scaled
+ * to the range actually scanned: what decides is the shared *fraction*, not
+ * the raw count. An unscaled comparison against HPAGE_PMD_NR/2 could never
+ * refuse a range this small, so both directions are checked here.
+ */
+static void collapse_order_sub_pmd_shared(struct collapse_context *c,
+					  struct mem_ops *ops)
+{
+	size_t window = mthp_window_size();
+	size_t size = 4 * window;
+	unsigned long nr_ptes = size / page_size;
+	unsigned long budget, cow;
+	int max_shared, wstatus;
+	void *p;
+
+	/*
+	 * The range has to stay strictly below a PMD to say anything about the
+	 * scaling: at exactly one PMD the scaled limit is the raw one, and the
+	 * case would pass without testing what it is here for.
+	 */
+	if (size >= hpage_pmd_size) {
+		ksft_test_result_skip("%s: four windows do not fit below the PMD\n",
+				      __func__);
+		return;
+	}
+
+	max_shared = thp_read_num("khugepaged/max_ptes_shared");
+	/* The same fraction of this range as max_shared is of a PMD. */
+	budget = (unsigned long)max_shared * nr_ptes / hpage_pmd_nr;
+	if (budget + 1 > nr_ptes) {
+		ksft_test_result_skip("%s: max_ptes_shared leaves nothing to exceed\n",
+				      __func__);
+		return;
+	}
+
+	mthp_push_target_order();
+
+	p = mmap(BASE_ADDR, size, PROT_READ | PROT_WRITE,
+		 MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+	if (p != BASE_ADDR)
+		ksft_exit_fail_msg("Failed to allocate VMA at %p\n", BASE_ADDR);
+	fill_memory(p, 0, size);
+	madvise(p, size, MADV_HUGEPAGE);
+
+	if (!fork()) {
+		/*
+		 * Everything is shared with the parent now. Break CoW on all
+		 * but budget + 1 PTEs: one PTE over the scaled limit, and far
+		 * below the unscaled one.
+		 */
+		cow = nr_ptes - budget - 1;
+		fill_memory(p, 0, cow * page_size);
+		ksft_print_msg("Refuse a sub-PMD range over the scaled max_ptes_shared...");
+		if (!khugepaged_wait_full_pass())
+			fail("Timeout");
+		else if (window_not_collapsed(p, size))
+			success("OK");
+		else
+			fail("Fail");
+
+		/* One fewer shared PTE brings it back within the limit. */
+		fill_memory(p, cow * page_size, (cow + 1) * page_size);
+		ksft_print_msg("Collapse once inside it...");
+		if (!khugepaged_wait_full_pass())
+			fail("Timeout");
+		else if (window_collapsed(p, size))
+			success("OK");
+		else
+			fail("Fail");
+
+		validate_memory(p, 0, size);
+		_exit(exit_status);
+	}
+	wait(&wstatus);
+	if (WEXITSTATUS(wstatus))
+		exit_status = WEXITSTATUS(wstatus);
+
+	munmap(p, size);
+	thp_pop_settings();
+	ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
 static void usage(void)
 {
 	fprintf(stderr, "\nUsage: ./khugepaged [OPTIONS] <test type> [dir]\n\n");
@@ -2084,6 +2168,7 @@ int main(int argc, char **argv)
 		TEST(collapse_order_mlocked, mthp_khugepaged_context, anon_ops);
 		TEST(collapse_order_lazyfree_window, mthp_khugepaged_context, anon_ops);
 		TEST(collapse_order_pinned_window, mthp_khugepaged_context, anon_ops);
+		TEST(collapse_order_sub_pmd_shared, mthp_khugepaged_context, anon_ops);
 	}
 
 	TEST(collapse_full, madvise_context, anon_ops);
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.