[PATCH 07/16] selftests/mm: cover a shared-source collapse write race
From: Kiryl Shutsemau
Date: Sun Aug 02 2026 - 16:00:34 EST
From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>
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) <kas@xxxxxxxxxx>
---
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