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. It passes on an unmodified kernel, so it pins down isolation that khugepaged collapse already provides. Assisted-by: Claude-Code:claude-opus-5 Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]> --- 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 d3ff1b196e09..a5683f08694f 100644 --- a/tools/testing/selftests/mm/khugepaged.c +++ b/tools/testing/selftests/mm/khugepaged.c @@ -1225,6 +1225,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) { @@ -1778,6 +1844,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

