[PATCH v4 17/19] selftests/mm: race collapse of windows with holes
From: Kiryl Shutsemau
Date: Fri Aug 14 2026 - 22:05:08 EST
From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>
The harness pins max_ptes_none to 0, so khugepaged only collapses a window
once every PTE in it is present. A window with holes takes a different
route, and never gets raced. A hole is zero-filled into the new folio by
clear_user_highpage() rather than copied from anywhere. Which slots count
as holes keeps moving under the racing MADV_DONTNEED, right up to the
moment the PMD is detached.
Run both ends of the occupancy scale for every driver mode, one after the
other. mTHP collapse supports only those two, 0 and HPAGE_PMD_NR - 1, and
coerces anything between them to 0. Each result says which end it ran:
ok 1 stepped/strict: 5s, 231 steps, no corruption
ok 2 stepped/holes: 5s, 194 steps, no corruption
Assisted-by: Claude-Code:claude-opus-5
Tested-by: Muhammad Usama Anjum <usama.anjum@xxxxxxx>
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
tools/testing/selftests/mm/khugepaged_race.c | 45 ++++++++++++--------
1 file changed, 28 insertions(+), 17 deletions(-)
diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index 3128031e28bc..3b369046edf5 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -11,9 +11,11 @@
* free khugepaged left to run (scan_sleep_millisecs=0), for soak;
* madvise MADV_COLLAPSE and MADV_DONTNEED in a loop.
*
- * All anon THP orders are enabled (inherit) and max_ptes_none is 0, so a
- * window has to be fully populated before khugepaged will collapse it, and
- * the racing MADV_DONTNEED decides which orders it can still use.
+ * All anon THP orders are enabled (inherit). Occupancy runs at both ends
+ * of what mTHP collapse supports: max_ptes_none 0, where a window must be
+ * fully populated, and HPAGE_PMD_NR - 1, where a window full of holes
+ * collapses too. A hole is zero-filled into the new folio rather than
+ * copied, and the racing MADV_DONTNEED keeps moving which slots are holes.
*
* Correctness signals: every racing page must read as its pattern or
* zero (MADV_DONTNEED), never anything else. The faulters and the fork
@@ -247,6 +249,8 @@ int main(int argc, char **argv)
const int nr_threads = ARRAY_SIZE(thread_names);
pthread_t threads[ARRAY_SIZE(thread_names)];
static const char * const all_modes[] = { "stepped", "free", "madvise" };
+ static const bool occupancies[] = { false, true }; /* strict, holes */
+ const int nr_occupancies = ARRAY_SIZE(occupancies);
const char *one_mode[1];
const char * const *modes = all_modes;
int nr_modes = ARRAY_SIZE(all_modes);
@@ -279,6 +283,7 @@ int main(int argc, char **argv)
usage();
}
}
+
if (mode_arg) {
if (strcmp(mode_arg, "stepped") && strcmp(mode_arg, "free") &&
strcmp(mode_arg, "madvise"))
@@ -318,7 +323,7 @@ int main(int argc, char **argv)
-1, 0) != (void *)mremap_scratch)
ksft_exit_fail_perror("mmap() mremap scratch");
- ksft_set_plan(nr_modes);
+ ksft_set_plan(nr_modes * nr_occupancies);
thp_save_settings();
thp_read_settings(&settings);
@@ -330,8 +335,9 @@ int main(int argc, char **argv)
*/
thp_push_settings(&settings);
- for (int m = 0; m < nr_modes; m++) {
- const char *mode = modes[m];
+ for (int mn = 0; mn < nr_modes * nr_occupancies; mn++) {
+ const char *mode = modes[mn / nr_occupancies];
+ bool holes = occupancies[mn % nr_occupancies];
thp_read_settings(&settings);
settings.thp_enabled = THP_MADVISE;
@@ -341,14 +347,16 @@ int main(int argc, char **argv)
settings.khugepaged.scan_sleep_millisecs =
strcmp(mode, "free") ? 1000 : 0;
settings.khugepaged.alloc_sleep_millisecs = 10;
+
/*
- * Strict occupancy: mTHP collapse only supports 0 or
- * HPAGE_PMD_NR - 1 and coerces anything else to 0 anyway, and 0
- * also keeps khugepaged from burning the whole step in doomed
- * PMD-sized allocations on 512M-PMD configs: under racing
- * MADV_DONTNEED a fully populated PMD area is rare.
+ * mTHP collapse only supports the two ends of the occupancy
+ * scale: 0 or HPAGE_PMD_NR - 1 (anything else coerces to 0).
+ * Strict needs a fully populated window, which is rare under
+ * racing MADV_DONTNEED; hole-heavy windows collapse instead,
+ * so the two ends race different paths.
*/
- settings.khugepaged.max_ptes_none = 0;
+ settings.khugepaged.max_ptes_none = holes ?
+ (hpage_pmd_size / page_size) - 1 : 0;
settings.khugepaged.pages_to_scan =
nr_areas * (hpage_pmd_size / page_size) * 8;
for (i = 0; i < NR_ORDERS; i++) {
@@ -415,8 +423,9 @@ int main(int argc, char **argv)
check_page(i);
ksft_test_result(!corrupted,
- "%s: %ds, %d steps, no corruption\n",
- mode, duration_s, steps);
+ "%s/%s: %ds, %d steps, no corruption\n",
+ mode, holes ? "holes" : "strict",
+ duration_s, steps);
/*
* Hand the address space and the settings back before the
@@ -430,9 +439,11 @@ int main(int argc, char **argv)
if (corrupted) {
/* Memory is suspect; the rest would prove nothing. */
- while (++m < nr_modes)
- ksft_test_result_skip("%s: skipped after corruption\n",
- modes[m]);
+ while (++mn < nr_modes * nr_occupancies)
+ ksft_test_result_skip("%s/%s: skipped after corruption\n",
+ modes[mn / nr_occupancies],
+ occupancies[mn % nr_occupancies] ?
+ "holes" : "strict");
break;
}
}
--
2.54.0