[PATCH 2/2] selftests/mm: add a uffd test for re-registering during UFFDIO_COPY
From: Jose A. Perez de Azpillaga
Date: Thu Oct 29 2026 - 18:12:30 EST
Re-register the destination without WP while a UFFDIO_COPY_MODE_WP copy
waits for its source page, then check that the copy fails with -EINVAL
and leaves no uffd-wp bit behind. This covers both the hugetlb and the
non-hugetlb UFFDIO_COPY retry.
Signed-off-by: Jose A. Perez de Azpillaga <azpijr@xxxxxxxxx>
---
tools/testing/selftests/mm/uffd-unit-tests.c | 105 +++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/tools/testing/selftests/mm/uffd-unit-tests.c b/tools/testing/selftests/mm/uffd-unit-tests.c
index 2a97ba1d6feb..8a7efaa7b3ac 100644
--- a/tools/testing/selftests/mm/uffd-unit-tests.c
+++ b/tools/testing/selftests/mm/uffd-unit-tests.c
@@ -375,6 +375,104 @@ static void uffd_wp_unpopulated_test(uffd_global_test_opts_t *gopts, uffd_test_a
uffd_test_pass();
}
+struct wp_copy_args {
+ uffd_global_test_opts_t *gopts;
+ char *src;
+ __s64 copy;
+};
+
+static void *wp_copy_thread(void *data)
+{
+ struct wp_copy_args *args = data;
+ struct uffdio_copy copy = {
+ .dst = (unsigned long)args->gopts->area_dst,
+ .src = (unsigned long)args->src,
+ .len = args->gopts->page_size,
+ .mode = UFFDIO_COPY_MODE_WP,
+ };
+
+ ioctl(args->gopts->uffd, UFFDIO_COPY, ©);
+ args->copy = copy.copy;
+
+ return NULL;
+}
+
+/*
+ * UFFDIO_COPY drops its locks to fault in the source page. Re-register the
+ * destination without WP while it waits there: the MODE_WP copy must then
+ * fail instead of leaving a uffd-wp bit in a VMA that no longer has WP.
+ */
+static void uffd_wp_copy_reregister_test(uffd_global_test_opts_t *gopts,
+ uffd_test_args_t *args)
+{
+ struct uffdio_api api = { .api = UFFD_API };
+ struct wp_copy_args copy_args = { .gopts = gopts };
+ unsigned long len = gopts->page_size;
+ struct uffdio_zeropage zeropage;
+ struct uffd_msg msg;
+ struct pollfd pfd;
+ pthread_t thread;
+ uint64_t value;
+ int pagemap_fd;
+ int src_uffd;
+
+ /* The source fault comes from the kernel, so no UFFD_USER_MODE_ONLY */
+ src_uffd = uffd_open(O_CLOEXEC);
+ if (src_uffd < 0) {
+ uffd_test_skip("cannot open a kernel-mode userfaultfd");
+ return;
+ }
+ if (ioctl(src_uffd, UFFDIO_API, &api))
+ err("UFFDIO_API failed");
+
+ copy_args.src = mmap(NULL, len, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (copy_args.src == MAP_FAILED)
+ err("mmap() failed");
+
+ if (uffd_register(src_uffd, copy_args.src, len, true, false, false))
+ err("register src failed");
+ if (uffd_register(gopts->uffd, gopts->area_dst, len, true, true, false))
+ err("register dst failed");
+
+ if (pthread_create(&thread, NULL, wp_copy_thread, ©_args))
+ err("pthread_create() failed");
+
+ /* Wait until UFFDIO_COPY blocks on the source page */
+ pfd = (struct pollfd){ .fd = src_uffd, .events = POLLIN };
+ if (poll(&pfd, 1, 10000) != 1)
+ err("no fault on the source page");
+ if (read(src_uffd, &msg, sizeof(msg)) != sizeof(msg))
+ err("read() failed");
+
+ if (uffd_unregister(gopts->uffd, gopts->area_dst, len))
+ err("unregister dst failed");
+ if (uffd_register(gopts->uffd, gopts->area_dst, len, true, false, false))
+ err("re-register dst failed");
+
+ zeropage.range.start = (unsigned long)copy_args.src;
+ zeropage.range.len = len;
+ zeropage.mode = 0;
+ if (ioctl(src_uffd, UFFDIO_ZEROPAGE, &zeropage))
+ err("UFFDIO_ZEROPAGE failed");
+
+ if (pthread_join(thread, NULL))
+ err("pthread_join() failed");
+
+ if (copy_args.copy != -EINVAL)
+ err("UFFDIO_COPY returned %lld, expected %d",
+ (long long)copy_args.copy, -EINVAL);
+
+ pagemap_fd = pagemap_open();
+ value = pagemap_get_entry(pagemap_fd, gopts->area_dst);
+ pagemap_check_wp(value, false);
+ close(pagemap_fd);
+
+ munmap(copy_args.src, len);
+ close(src_uffd);
+ uffd_test_pass();
+}
+
static void uffd_wp_fork_test_common(uffd_global_test_opts_t *gopts, uffd_test_args_t *args,
bool with_event)
{
@@ -2403,6 +2501,13 @@ uffd_test_case_t uffd_tests[] = {
.uffd_feature_required =
UFFD_FEATURE_PAGEFAULT_FLAG_WP | UFFD_FEATURE_WP_UNPOPULATED,
},
+ {
+ .name = "wp-copy-reregister",
+ .uffd_fn = uffd_wp_copy_reregister_test,
+ .mem_targets = MEM_ALL,
+ .uffd_feature_required = UFFD_FEATURE_PAGEFAULT_FLAG_WP |
+ UFFD_FEATURE_WP_HUGETLBFS_SHMEM,
+ },
{
.name = "minor",
.uffd_fn = uffd_minor_test,
--
2.55.0