[PATCH v4 13/19] selftests/mm: cover a shared-source collapse write race
From: Kiryl Shutsemau
Date: Fri Aug 14 2026 - 22:01:01 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.
The co-sharer unshares one page every 10ms, and only once the collapsing
side says it is about to start. Writing the range in a burst breaks CoW
on all of it before the collapse begins, leaving the child to collapse
pages that are already exclusive to it.
Nothing here is new behaviour: the case passes on mainline, and locks in
isolation that 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 | 94 +++++++++++++++++++++++++
1 file changed, 94 insertions(+)
diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 1844ddd77b59..856decd2950a 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1184,6 +1184,97 @@ 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;
+ pid_t child;
+ int sync[2];
+ char go = 1;
+ void *p;
+
+ p = ops->setup_area(1);
+ ip = p;
+ ops->fault(p, 0, shared); /* shared prefix, pre-fork pattern */
+ if (pipe(sync))
+ ksft_exit_fail_perror("pipe()");
+
+ ksft_print_msg("Fork, collapse in the child while the parent rewrites...");
+ child = fork();
+ if (!child) {
+ int collapse_status;
+
+ close(sync[0]);
+ ops->fault(p, shared, hpage_pmd_size); /* private remainder */
+ /* Start the parent unsharing, and give it a head start. */
+ if (write(sync[1], &go, 1) != 1)
+ _exit(KSFT_FAIL);
+ usleep(5000);
+ 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);
+ }
+
+ close(sync[1]);
+ if (read(sync[0], &go, 1) != 1)
+ ksft_exit_fail_msg("child never reached the collapse\n");
+
+ /*
+ * Unshare one page at a time. A burst would break CoW on all of them
+ * in microseconds -- wait_for_scan() does not even poll for TICK --
+ * and the child would collapse pages already exclusive to it.
+ */
+ i = 0;
+ do {
+ if (i < n)
+ ip[i * stride] = i + 0xbeef0000;
+ i++;
+ usleep(10 * 1000);
+ } while (waitpid(child, &wstatus, WNOHANG) == 0);
+
+ /* Whatever the paced sweep did not reach, so the check below is exact. */
+ for (; i < n; i++)
+ ip[i * stride] = i + 0xbeef0000;
+ /* 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)
{
@@ -1740,6 +1831,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