selftests/mm: add HMM test for mmap lock-dropping faults

Add test_hmm coverage for the HMM lock-dropping fault path.  The test
module gets a new HMM_DMIRROR_READ_UNLOCKED ioctl that calls
hmm_range_fault_unlocked_timeout() with a timeout of 0, exercising the
unbounded retry mode while allowing the mmap lock to be dropped during
fault handling.

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.

Assisted-by: GitHub-Copilot:claude-opus-4.6
Link: https://lore.kernel.org/20260723-hmm-v10-v11-3-c55b003a4b61@gmail.com
Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: Dave Airlie <airlied@gmail.com>
Cc: David Hildenbrand <david@kernel.org>
Cc: Dexuan Cui <decui@microsoft.com>
Cc: Haiyang Zhang <haiyangz@microsoft.com>
Cc: Jason Gunthorpe <jgg@nvidia.com>
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: K. Y. Srinivasan <kys@microsoft.com>
Cc: Leon Romanovsky <leon@kernel.org>
Cc: Liam R. Howlett <liam@infradead.org>
Cc: Lizhi Hou <lizhi.hou@amd.com>
Cc: Long Li <longli@microsoft.com>
Cc: Lorenzo Stoakes <ljs@kernel.org>
Cc: Lyude <lyude@redhat.com>
Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
Cc: Maxime Ripard <mripard@kernel.org>
Cc: Michal Hocko <mhocko@suse.com>
Cc: Mike Rapoport <rppt@kernel.org>
Cc: Oded Gabbay <ogabbay@kernel.org>
Cc: Shuah Khan <shuah@kernel.org>
Cc: Suren Baghdasaryan <surenb@google.com>
Cc: Thomas Zimemrmann <tzimmermann@suse.de>
Cc: Vlastimil Babka <vbabka@kernel.org>
Cc: Wei Liu <wei.liu@kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
This commit is contained in:
Stanislav Kinsburskii
2026-08-04 19:18:54 -07:00
committed by Andrew Morton
parent 1211708312
commit 9365813cad
3 changed files with 257 additions and 1 deletions
+106 -1
View File
@@ -389,6 +389,67 @@ out:
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,7 +1675,9 @@ 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;
default:
return -EINVAL;
}
+1
View File
@@ -38,6 +38,7 @@ 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_FLAG_FAIL_ALLOC (1ULL << 0)
+150
View File
@@ -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
@@ -2952,4 +2956,150 @@ 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;
}
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_HARNESS_MAIN