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

