Re: [PATCH v2] mm/selftest: Fix race condition in userfaultfd dynamic address allocation

Marty Kareem
2025-04-17T18:18:58Z

Hello,

I'm writing to follow up on the patch I submitted regarding improved dynamic address allocation in userfaultfd tests. The patch aims to prevent race conditions by keeping temporary PROT_NONE reservations active until they can be atomically replaced with MAP_FIXED mappings.

I sent the patch through git send and I was wondering if you've had a chance to review this submission and if you have any feedback or questions about the implementation. As mentioned in the original submission, this approach makes the tests more reliable, especially when running in parallel, with minimal performance impact.

Thank you for your time. I look forward to hearing from you.

Best regards,
Marty Kareem

________________________________________
From: Marty Kareem <martykareem@outlook.com>
Sent: Thursday, March 27, 2025 11:20 PM
To: Peter Xu
Cc: linux-mm@kvack.org; linux-kernel@vger.kernel.org; akpm@linux-foundation.org; shuah@kernel.org; MrMartyK
Subject: [PATCH v2] mm/selftest: Fix race condition in userfaultfd dynamic address allocation

This patch improves the dynamic address allocation in userfaultfd tests to
prevent potential race conditions. Instead of unmapping the PROT_NONE
reservation before mapping to that area, we now keep the temporary
reservation active until we can atomically replace it with MAP_FIXED.

Key changes:
1. Keep PROT_NONE reservation active until ready to use
2. Use MAP_FIXED to atomically replace reservation
3. Remove MAP_FIXED_NOREPLACE conditionals since atomic replacement works
on all kernel versions
4. Simplify overall implementation while maintaining robustness

This approach prevents race conditions where another thread might grab the
memory area between unmapping and remapping, making the tests more reliable
especially when running in parallel.

Performance impact is minimal (approximately 1.3x overhead vs static
addressing) while significantly improving reliability.
---
tools/testing/selftests/mm/uffd-common.c | 113 ++++++++---------------
1 file changed, 39 insertions(+), 74 deletions(-)

diff --git a/tools/testing/selftests/mm/uffd-common.c b/tools/testing/selftests/mm/uffd-common.c
index 56a69c6cc7c4..fab3b79abc15 100644
--- a/tools/testing/selftests/mm/uffd-common.c
+++ b/tools/testing/selftests/mm/uffd-common.c
@@ -123,54 +123,22 @@ static void shmem_release_pages(char *rel_area)
}

/**
- * Structure to hold the reservation and aligned address information
- * This helps prevent race conditions by keeping the original reservation
- * active until it can be atomically replaced with the real mapping.
- */
-struct addr_mapping {
- void *reservation; /* The original memory reservation */
- void *aligned_addr; /* The aligned address for actual use */
- size_t size; /* Size of the reservation */
-};
-
-/**
- * Find a suitable virtual address area of the requested size and alignment
+ * Find a suitable virtual address area of the requested size
*
- * This function obtains a hint from the OS about where a good place to map
- * memory might be, creates a temporary reservation to hold the space, and
- * calculates an aligned address within that reservation.
+ * This function creates a temporary reservation with PROT_NONE to hold
+ * the address space. This reservation prevents other threads from taking
+ * the address range until we can atomically replace it with our real mapping.
*
- * IMPORTANT: The caller must eventually unmap the reservation when done
- * or replace it with MAP_FIXED to prevent memory leaks.
+ * IMPORTANT: The caller must eventually replace this reservation with MAP_FIXED
+ * or munmap it to prevent memory leaks.
*
- * @param mapping Pointer to addr_mapping struct that will receive the results
* @param size Size of the memory area needed
- * @param alignment Alignment requirement (typically huge page size)
- * @return 0 on success, -1 on failure
+ * @return Reserved memory area or NULL on failure
*/
-static int find_suitable_area(struct addr_mapping *mapping, size_t size, size_t alignment)
+static void *find_suitable_area(size_t size)
{
- void *addr;
- uintptr_t aligned_addr;
-
- if (!mapping)
- return -1;
-
- /* First create a reservation with PROT_NONE to hold the address space */
- addr = mmap(NULL, size, PROT_NONE,
- MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- if (addr == MAP_FAILED)
- return -1;
-
- /* Calculate an aligned address within this reservation */
- aligned_addr = ((uintptr_t)addr + alignment - 1) & ~(alignment - 1);
-
- /* Store both the reservation and the aligned address */
- mapping->reservation = addr;
- mapping->aligned_addr = (void *)aligned_addr;
- mapping->size = size;
-
- return 0;
+ /* Create a reservation with PROT_NONE to hold the address space */
+ return mmap(NULL, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
}

static int shmem_allocate_area(void **alloc_area, bool is_src)
@@ -179,13 +147,12 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
size_t bytes = nr_pages * page_size, hpage_size = read_pmd_pagesize();
unsigned long offset = is_src ? 0 : bytes;
int mem_fd = uffd_mem_fd_create(bytes * 2, false);
- struct addr_mapping addr_map = {0};
- struct addr_mapping alias_map = {0};
- int ret;
+ void *reserved_area = NULL;
+ void *reserved_alias = NULL;

- /* Get a suitable address space with reservation */
- ret = find_suitable_area(&addr_map, bytes, hpage_size);
- if (ret < 0) {
+ /* Get a suitable address reservation */
+ reserved_area = find_suitable_area(bytes);
+ if (reserved_area == MAP_FAILED) {
/* Couldn't get a reservation, but we can still try without hints */
*alloc_area = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED, mem_fd, offset);
@@ -195,21 +162,22 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
return -errno;
}
} else {
- void *target_addr = addr_map.aligned_addr;
+ void *target_addr = reserved_area;

/* If this is dst area, add offset to prevent overlap with src area */
if (!is_src) {
+ /* Unmap the original reservation since we're using a different address */
+ munmap(reserved_area, bytes);
+
/* Calculate new address with the same spacing as original code */
/* src map + alias + interleaved hpages */
- uintptr_t new_addr = (uintptr_t)target_addr +
- 2 * (bytes + hpage_size);
-
- /* Unmap the original reservation since we're using a different address */
- munmap(addr_map.reservation, addr_map.size);
+ target_addr = (char *)reserved_area + 2 * (bytes + hpage_size);

/* Create a new reservation at the offset location */
- ret = find_suitable_area(&addr_map, bytes, hpage_size);
- if (ret < 0) {
+ reserved_area = mmap(target_addr, bytes, PROT_NONE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (reserved_area == MAP_FAILED) {
/* Fallback to non-fixed mapping if we can't reserve space */
*alloc_area = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED, mem_fd, offset);
@@ -220,7 +188,7 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
}
} else {
/* Use our new reservation */
- target_addr = addr_map.aligned_addr;
+ target_addr = reserved_area;
}
}

@@ -233,14 +201,11 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
*alloc_area = mmap(target_addr, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, mem_fd, offset);

- /* Check if the mapping succeeded at our target address */
- if (*alloc_area == MAP_FAILED || *alloc_area != target_addr) {
+ if (*alloc_area == MAP_FAILED) {
/* If fixed mapping failed, clean up and try anywhere */
- if (*alloc_area != MAP_FAILED)
- munmap(*alloc_area, bytes);
-
- /* Clean up the reservation if it's still around */
- munmap(addr_map.reservation, addr_map.size);
+ /* Explicitly munmap the reservation since our map failed */
+ if (reserved_area != MAP_FAILED)
+ munmap(reserved_area, bytes);

/* Fall back to letting the kernel choose an address */
*alloc_area = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
@@ -254,12 +219,12 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
}

/* Calculate a good spot for the alias mapping with space to prevent merging */
- ret = find_suitable_area(&alias_map, bytes, hpage_size);
- if (ret < 0) {
+ void *p_alias = (char *)((uintptr_t)*alloc_area + bytes + hpage_size);
+
+ /* Reserve space for alias mapping */
+ reserved_alias = find_suitable_area(bytes);
+ if (reserved_alias == MAP_FAILED) {
/* Fallback to using an offset from the first mapping */
- void *p_alias = (char *)((uintptr_t)*alloc_area + bytes + hpage_size);
-
- /* No reservation, map directly */
area_alias = mmap(p_alias, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, mem_fd, offset);

@@ -270,14 +235,14 @@ static int shmem_allocate_area(void **alloc_area, bool is_src)
}
} else {
/* Use our reservation for the alias mapping */
- area_alias = mmap(alias_map.aligned_addr, bytes, PROT_READ | PROT_WRITE,
+ area_alias = mmap(reserved_alias, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, mem_fd, offset);

- /* Whether it succeeded or failed, we need to clean up the reservation */
- if (area_alias != alias_map.aligned_addr)
- munmap(alias_map.reservation, alias_map.size);
-
+ /* If mapping failed, try without specific address */
if (area_alias == MAP_FAILED) {
+ /* Clean up the reservation since it didn't get replaced */
+ munmap(reserved_alias, bytes);
+
/* If fixed mapping failed, try anywhere */
area_alias = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED, mem_fd, offset);
--
2.43.0