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


Reply via email to