Add test_hmm coverage for the HMM lock-dropping fault path. The test module
gets new HMM_DMIRROR_READ_UNLOCKED and HMM_DMIRROR_READ_UNLOCKED_TIMEOUT
ioctls that both call hmm_range_fault_unlocked_timeout(). The unlocked
ioctl passes a timeout of 0 to exercise the unbounded retry mode, while the
timeout ioctl passes HMM_RANGE_DEFAULT_TIMEOUT converted to jiffies.

Add a userfaultfd_read selftest that registers an anonymous mapping with
UFFDIO_REGISTER_MODE_MISSING, services the faults from a handler thread
with UFFDIO_COPY, and verifies that HMM can read back the data supplied by
the handler. This exercises the path where handle_mm_fault() drops
mmap_lock and hmm_range_fault_unlocked_timeout() restarts the walk
internally.

Add a userfaultfd_timeout selftest for the finite-timeout mode. It
registers a missing-mode userfaultfd range without a data-supplying
handler, then closes the userfaultfd after the timeout window so the
blocked fault can unwind and hmm_range_fault_unlocked_timeout() can return
-EBUSY.

Assisted-by: GitHub-Copilot:claude-opus-4.6
Signed-off-by: Stanislav Kinsburskii <[email protected]>
---
 lib/test_hmm.c                         |  111 ++++++++++++++++
 lib/test_hmm_uapi.h                    |    3 
 tools/testing/selftests/mm/hmm-tests.c |  220 ++++++++++++++++++++++++++++++++
 3 files changed, 334 insertions(+)

diff --git a/lib/test_hmm.c b/lib/test_hmm.c
index 45c0cb992218..02a98887c290 100644
--- a/lib/test_hmm.c
+++ b/lib/test_hmm.c
@@ -389,6 +389,67 @@ static int dmirror_range_fault(struct dmirror *dmirror,
        return ret;
 }
 
+static int dmirror_range_fault_unlocked(struct dmirror *dmirror,
+                                       struct hmm_range *range,
+                                       unsigned long timeout)
+{
+       int ret;
+
+       while (true) {
+               ret = hmm_range_fault_unlocked_timeout(range, timeout);
+               if (ret)
+                       goto out;
+
+               mutex_lock(&dmirror->mutex);
+               if (mmu_interval_read_retry(range->notifier,
+                                           range->notifier_seq)) {
+                       mutex_unlock(&dmirror->mutex);
+                       continue;
+               }
+               break;
+       }
+
+       ret = dmirror_do_fault(dmirror, range);
+
+       mutex_unlock(&dmirror->mutex);
+out:
+       return ret;
+}
+
+static int dmirror_fault_unlocked(struct dmirror *dmirror,
+                                 unsigned long start,
+                                 unsigned long end, bool write,
+                                 unsigned long timeout)
+{
+       struct mm_struct *mm = dmirror->notifier.mm;
+       unsigned long addr;
+       unsigned long pfns[32];
+       struct hmm_range range = {
+               .notifier = &dmirror->notifier,
+               .hmm_pfns = pfns,
+               .pfn_flags_mask = 0,
+               .default_flags =
+                       HMM_PFN_REQ_FAULT | (write ? HMM_PFN_REQ_WRITE : 0),
+               .dev_private_owner = dmirror->mdevice,
+       };
+       int ret = 0;
+
+       if (!mmget_not_zero(mm))
+               return -EFAULT;
+
+       for (addr = start; addr < end; addr = range.end) {
+               range.start = addr;
+               range.end = min(addr + (ARRAY_SIZE(pfns) << PAGE_SHIFT), end);
+
+               ret = dmirror_range_fault_unlocked(dmirror, &range, timeout);
+               if (ret)
+                       break;
+       }
+
+       mmput(mm);
+       return ret;
+}
+
 static int dmirror_fault(struct dmirror *dmirror, unsigned long start,
                         unsigned long end, bool write)
 {
@@ -488,6 +549,48 @@ static int dmirror_read(struct dmirror *dmirror, struct 
hmm_dmirror_cmd *cmd)
        return ret;
 }
 
+static int dmirror_read_unlocked(struct dmirror *dmirror,
+                                struct hmm_dmirror_cmd *cmd,
+                                unsigned long timeout)
+{
+       struct dmirror_bounce bounce;
+       unsigned long start, end;
+       unsigned long size = cmd->npages << PAGE_SHIFT;
+       int ret;
+
+       start = cmd->addr;
+       end = start + size;
+       if (end < start)
+               return -EINVAL;
+
+       ret = dmirror_bounce_init(&bounce, start, size);
+       if (ret)
+               return ret;
+
+       while (1) {
+               mutex_lock(&dmirror->mutex);
+               ret = dmirror_do_read(dmirror, start, end, &bounce);
+               mutex_unlock(&dmirror->mutex);
+               if (ret != -ENOENT)
+                       break;
+
+               start = cmd->addr + (bounce.cpages << PAGE_SHIFT);
+               ret = dmirror_fault_unlocked(dmirror, start, end, false, 
timeout);
+               if (ret)
+                       break;
+               cmd->faults++;
+       }
+
+       if (ret == 0) {
+               if (copy_to_user(u64_to_user_ptr(cmd->ptr), bounce.ptr,
+                                bounce.size))
+                       ret = -EFAULT;
+       }
+       cmd->cpages = bounce.cpages;
+       dmirror_bounce_fini(&bounce);
+       return ret;
+}
+
 static int dmirror_do_write(struct dmirror *dmirror, unsigned long start,
                            unsigned long end, struct dmirror_bounce *bounce)
 {
@@ -1572,6 +1675,14 @@ static long dmirror_fops_unlocked_ioctl(struct file 
*filp,
                dmirror->flags = cmd.npages;
                ret = 0;
                break;
+       case HMM_DMIRROR_READ_UNLOCKED:
+               ret = dmirror_read_unlocked(dmirror, &cmd, 0);
+               break;
+       case HMM_DMIRROR_READ_UNLOCKED_TIMEOUT:
+               ret = dmirror_read_unlocked(dmirror, &cmd,
+                               msecs_to_jiffies(
+                                       HMM_RANGE_DEFAULT_TIMEOUT));
+               break;
 
        default:
                return -EINVAL;
diff --git a/lib/test_hmm_uapi.h b/lib/test_hmm_uapi.h
index f94c6d457338..249ab2d9e3b2 100644
--- a/lib/test_hmm_uapi.h
+++ b/lib/test_hmm_uapi.h
@@ -38,6 +38,9 @@ struct hmm_dmirror_cmd {
 #define HMM_DMIRROR_CHECK_EXCLUSIVE    _IOWR('H', 0x06, struct hmm_dmirror_cmd)
 #define HMM_DMIRROR_RELEASE            _IOWR('H', 0x07, struct hmm_dmirror_cmd)
 #define HMM_DMIRROR_FLAGS              _IOWR('H', 0x08, struct hmm_dmirror_cmd)
+#define HMM_DMIRROR_READ_UNLOCKED      _IOWR('H', 0x09, struct hmm_dmirror_cmd)
+#define HMM_DMIRROR_READ_UNLOCKED_TIMEOUT \
+       _IOWR('H', 0x0a, struct hmm_dmirror_cmd)
 
 #define HMM_DMIRROR_FLAG_FAIL_ALLOC    (1ULL << 0)
 
diff --git a/tools/testing/selftests/mm/hmm-tests.c 
b/tools/testing/selftests/mm/hmm-tests.c
index 2f2b9879d100..dc031d2e2aae 100644
--- a/tools/testing/selftests/mm/hmm-tests.c
+++ b/tools/testing/selftests/mm/hmm-tests.c
@@ -29,6 +29,10 @@
 #include <sys/mman.h>
 #include <sys/ioctl.h>
 #include <sys/time.h>
+#include <sys/syscall.h>
+#include <sys/eventfd.h>
+#include <linux/userfaultfd.h>
+#include <poll.h>
 
 /*
  * This is a private UAPI to the kernel test module so it isn't exported
@@ -2949,4 +2953,220 @@ TEST_F_TIMEOUT(hmm, benchmark_thp_migration, 120)
                                        &thp_results, &regular_results);
        }
 }
+/*
+ * Test that HMM can fault in pages backed by userfaultfd using the
+ * hmm_range_fault_unlocked_timeout() path with no timeout. This exercises
+ * the lock-drop retry logic in the HMM framework.
+ */
+struct uffd_thread_args {
+       int uffd;
+       int stop_fd;
+       void *page_buffer;
+       unsigned long page_size;
+};
+
+static void *uffd_handler_thread(void *arg)
+{
+       struct uffd_thread_args *args = arg;
+       struct uffd_msg msg;
+       struct uffdio_copy copy;
+       struct pollfd pollfd[2];
+       int ret;
+
+       pollfd[0].fd = args->uffd;
+       pollfd[0].events = POLLIN;
+       pollfd[1].fd = args->stop_fd;
+       pollfd[1].events = POLLIN;
+
+       while (1) {
+               ret = poll(pollfd, 2, -1);
+               if (ret <= 0)
+                       break;
+               if (pollfd[1].revents)
+                       break;
+               if (!(pollfd[0].revents & POLLIN))
+                       break;
+
+               ret = read(args->uffd, &msg, sizeof(msg));
+               if (ret != sizeof(msg))
+                       break;
+
+               if (msg.event != UFFD_EVENT_PAGEFAULT)
+                       break;
+
+               /* Fill the page with a known pattern */
+               memset(args->page_buffer, 0xAB, args->page_size);
+
+               copy.dst = msg.arg.pagefault.address & ~(args->page_size - 1);
+               copy.src = (unsigned long)args->page_buffer;
+               copy.len = args->page_size;
+               copy.mode = 0;
+               copy.copy = 0;
+
+               ret = ioctl(args->uffd, UFFDIO_COPY, &copy);
+               if (ret < 0)
+                       break;
+       }
+
+       return NULL;
+}
+
+struct uffd_close_args {
+       int uffd;
+       unsigned int delay_us;
+};
+
+static void *uffd_close_thread(void *arg)
+{
+       struct uffd_close_args *args = arg;
+
+       usleep(args->delay_us);
+       close(args->uffd);
+       return NULL;
+}
+
+TEST_F(hmm, userfaultfd_read)
+{
+       struct hmm_buffer *buffer;
+       struct uffd_thread_args uffd_args;
+       unsigned long npages;
+       unsigned long size;
+       unsigned long i;
+       unsigned char *ptr;
+       pthread_t thread;
+       int uffd;
+       int stop_fd;
+       int ret;
+       struct uffdio_api api;
+       struct uffdio_register reg;
+       uint64_t stop = 1;
+       ssize_t nwrite;
+
+       npages = 4;
+       size = npages << self->page_shift;
+
+       /* Create userfaultfd */
+       uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+       if (uffd < 0)
+               SKIP(return, "userfaultfd not available");
+
+       api.api = UFFD_API;
+       api.features = 0;
+       ret = ioctl(uffd, UFFDIO_API, &api);
+       ASSERT_EQ(ret, 0);
+
+       buffer = malloc(sizeof(*buffer));
+       ASSERT_NE(buffer, NULL);
+
+       buffer->fd = -1;
+       buffer->size = size;
+       buffer->mirror = malloc(size);
+       ASSERT_NE(buffer->mirror, NULL);
+
+       /* Create anonymous mapping */
+       buffer->ptr = mmap(NULL, size,
+                          PROT_READ | PROT_WRITE,
+                          MAP_PRIVATE | MAP_ANONYMOUS,
+                          -1, 0);
+       ASSERT_NE(buffer->ptr, MAP_FAILED);
+
+       /* Register the region with userfaultfd */
+       reg.range.start = (unsigned long)buffer->ptr;
+       reg.range.len = size;
+       reg.mode = UFFDIO_REGISTER_MODE_MISSING;
+       ret = ioctl(uffd, UFFDIO_REGISTER, &reg);
+       ASSERT_EQ(ret, 0);
+
+       /* Set up the handler thread */
+       uffd_args.uffd = uffd;
+       stop_fd = eventfd(0, EFD_CLOEXEC);
+       ASSERT_GE(stop_fd, 0);
+       uffd_args.stop_fd = stop_fd;
+       uffd_args.page_buffer = malloc(self->page_size);
+       ASSERT_NE(uffd_args.page_buffer, NULL);
+       uffd_args.page_size = self->page_size;
+
+       ret = pthread_create(&thread, NULL, uffd_handler_thread, &uffd_args);
+       ASSERT_EQ(ret, 0);
+
+       /*
+        * Use the unlocked read path which allows the mmap lock to be
+        * dropped during the fault, enabling userfaultfd resolution.
+        */
+       ret = hmm_dmirror_cmd(self->fd, HMM_DMIRROR_READ_UNLOCKED,
+                             buffer, npages);
+       ASSERT_EQ(ret, 0);
+       ASSERT_EQ(buffer->cpages, npages);
+
+       /* Verify the device read the data filled by the uffd handler */
+       ptr = buffer->mirror;
+       for (i = 0; i < size; ++i)
+               ASSERT_EQ(ptr[i], (unsigned char)0xAB);
+
+       nwrite = write(stop_fd, &stop, sizeof(stop));
+       ASSERT_EQ(nwrite, sizeof(stop));
+       pthread_join(thread, NULL);
+       close(stop_fd);
+       free(uffd_args.page_buffer);
+       close(uffd);
+       hmm_buffer_free(buffer);
+}
+
+TEST_F(hmm, userfaultfd_timeout)
+{
+       struct uffd_close_args close_args;
+       struct hmm_buffer *buffer;
+       unsigned long npages;
+       unsigned long size;
+       pthread_t thread;
+       int uffd;
+       int ret;
+       struct uffdio_api api;
+       struct uffdio_register reg;
+
+       npages = 4;
+       size = npages << self->page_shift;
+
+       uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+       if (uffd < 0)
+               SKIP(return, "userfaultfd not available");
+
+       api.api = UFFD_API;
+       api.features = 0;
+       ret = ioctl(uffd, UFFDIO_API, &api);
+       ASSERT_EQ(ret, 0);
+
+       buffer = malloc(sizeof(*buffer));
+       ASSERT_NE(buffer, NULL);
+
+       buffer->fd = -1;
+       buffer->size = size;
+       buffer->mirror = malloc(size);
+       ASSERT_NE(buffer->mirror, NULL);
+
+       buffer->ptr = mmap(NULL, size,
+                          PROT_READ | PROT_WRITE,
+                          MAP_PRIVATE | MAP_ANONYMOUS,
+                          -1, 0);
+       ASSERT_NE(buffer->ptr, MAP_FAILED);
+
+       reg.range.start = (unsigned long)buffer->ptr;
+       reg.range.len = size;
+       reg.mode = UFFDIO_REGISTER_MODE_MISSING;
+       ret = ioctl(uffd, UFFDIO_REGISTER, &reg);
+       ASSERT_EQ(ret, 0);
+
+       close_args.uffd = uffd;
+       close_args.delay_us = 1500 * 1000;
+       ret = pthread_create(&thread, NULL, uffd_close_thread, &close_args);
+       ASSERT_EQ(ret, 0);
+
+       ret = hmm_dmirror_cmd(self->fd, HMM_DMIRROR_READ_UNLOCKED_TIMEOUT,
+                           buffer, npages);
+       pthread_join(thread, NULL);
+       ASSERT_EQ(ret, -EBUSY);
+
+       hmm_buffer_free(buffer);
+}
+
 TEST_HARNESS_MAIN



Reply via email to