From: "Kiryl Shutsemau (Meta)" collapse_fork already checks that a fork-shared range collapses in the process that asks while a co-sharer keeps its own page, but with the co-sharer quiescent. Add a case that keeps a co-sharer writing to the shared range throughout the collapse: CoW must keep the two sides' content independent -- the collapsing child sees the pre-fork content, the writing parent sees only its own writes. This exercises the requirement that CoW keeps a shared source isolated from the collapsing side throughout the collapse, even under an actively writing co-sharer. Passes on the unmodified kernel, pinning down the CoW-isolation contract that khugepaged collapse already provides. Assisted-by: Claude-Code:claude-opus-5 Signed-off-by: Kiryl Shutsemau (Meta) --- tools/testing/selftests/mm/khugepaged.c | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c index 65fafab06410..81001e15765c 100644 --- a/tools/testing/selftests/mm/khugepaged.c +++ b/tools/testing/selftests/mm/khugepaged.c @@ -1047,6 +1047,61 @@ 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, i, n = shared / page_size; + 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()) { + ops->fault(p, shared, hpage_pmd_size); /* private remainder */ + c->collapse("Collapse a range shared with a writing co-sharer", + p, 1, ops, true); + for (i = 0; i < n; i++) + if (ip[i * stride] != i + 0xdead0000) { + fail("Fail: child content"); + ops->cleanup_area(p, hpage_pmd_size); + _exit(exit_status); + } + success("OK"); + 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); + exit_status = WEXITSTATUS(wstatus); + + 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); + ksft_test_result_report(exit_status, "%s\n", __func__); +} + static void madvise_collapse_existing_thps(struct collapse_context *c, struct mem_ops *ops) { @@ -1595,6 +1650,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