From: "Kiryl Shutsemau (Meta)" <[email protected]>

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 <[email protected]>
Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 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


Reply via email to