[PATCH v3 12/18] selftests/mm: cover a shared-source collapse write race

From: Kiryl Shutsemau

Date: Wed Aug 12 2026 - 09:28:37 EST


From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>

collapse_fork checks that a fork-shared range collapses in the process
that asks for it while the co-sharer keeps its own page, but the co-sharer
sits still while that happens.

Add a case where the co-sharer writes to the shared range throughout the
collapse. CoW has to keep the two sides apart under those writes: the
collapsing child must see the content from before the fork, and the
writing parent must see only its own writes.

It passes on an unmodified kernel, so it pins down isolation that
khugepaged collapse already provides.

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.c | 69 +++++++++++++++++++++++++
1 file changed, 69 insertions(+)

diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 3099e7b720d9..9c8830fed0a2 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1207,6 +1207,72 @@ static void collapse_max_ptes_shared(struct collapse_context *c, struct mem_ops
ksft_test_result_report(exit_status, "%s\n", __func__);
}

+/*
+ * Content stays isolated while a co-sharer writes concurrently. A shared
+ * source is copied live (not frozen), relying on it being CoW - immutable
+ * for the duration of the copy; a co-sharer's write goes to a CoW copy. The
+ * collapsing child must see the pre-fork content, the writing parent only
+ * its own writes.
+ */
+static void collapse_fork_cow_race(struct collapse_context *c, struct mem_ops *ops)
+{
+ const unsigned long shared = 64 * page_size;
+ const int stride = page_size / sizeof(int);
+ int wstatus, child_status, i, n = shared / page_size;
+ /* volatile: the loop below must really store, on every iteration */
+ volatile int *ip;
+ void *p;
+
+ p = ops->setup_area(1);
+ ip = p;
+ ops->fault(p, 0, shared); /* shared prefix, pre-fork pattern */
+
+ ksft_print_msg("Fork, collapse in the child while the parent rewrites...");
+ if (!fork()) {
+ int collapse_status;
+
+ ops->fault(p, shared, hpage_pmd_size); /* private remainder */
+ c->collapse("Collapse a range shared with a writing co-sharer",
+ p, 1, ops, true);
+ collapse_status = exit_status;
+ for (i = 0; i < n; i++)
+ if (ip[i * stride] != i + 0xdead0000)
+ break;
+ if (i == n)
+ success("OK");
+ else
+ fail("Fail: child content");
+ /* The content check must not bury a failed collapse. */
+ if (exit_status != KSFT_FAIL)
+ exit_status = collapse_status;
+ ops->cleanup_area(p, hpage_pmd_size);
+ _exit(exit_status);
+ }
+
+ /* Hammer the parent's own writes over the shared prefix. */
+ for (int it = 0; it < 200000; it++)
+ for (i = 0; i < n; i++)
+ ip[i * stride] = i + 0xbeef0000;
+
+ wait(&wstatus);
+ /* A child that died reading the racing pages is a failure, not a zero. */
+ child_status = WIFEXITED(wstatus) ? WEXITSTATUS(wstatus) : KSFT_FAIL;
+
+ ksft_print_msg("Check the parent sees only its own writes...");
+ for (i = 0; i < n; i++)
+ if (ip[i * stride] != i + 0xbeef0000)
+ break;
+ if (i == n)
+ success("OK");
+ else
+ fail("Fail: parent content");
+ ops->cleanup_area(p, hpage_pmd_size);
+ /* Same again: our own check must not bury the child's verdict. */
+ if (exit_status != KSFT_FAIL)
+ exit_status = child_status;
+ ksft_test_result_report(exit_status, "%s\n", __func__);
+}
+
static void madvise_collapse_existing_thps(struct collapse_context *c,
struct mem_ops *ops)
{
@@ -1770,6 +1836,9 @@ int main(int argc, char **argv)
TEST(collapse_max_ptes_shared, khugepaged_context, anon_ops);
TEST(collapse_max_ptes_shared, madvise_context, anon_ops);

+ TEST(collapse_fork_cow_race, khugepaged_context, anon_ops);
+ TEST(collapse_fork_cow_race, madvise_context, anon_ops);
+
TEST(madvise_collapse_existing_thps, madvise_context, anon_ops);
TEST(madvise_collapse_existing_thps, madvise_context, read_only_file_ops);
TEST(madvise_collapse_existing_thps, madvise_context, read_write_file_read_ops);
--
2.54.0