When MREMAP_FIXED moves a range spanning multiple VMAs, remap_move()
iterates over the source VMAs and skips source holes. For each source
VMA segment it narrows vrm->new_len to that segment length before calling
mremap_to(), so the MREMAP_FIXED destination unmap is also limited to the
current segment.
If the source range contains holes and the destination range is already
mapped, the destination subranges corresponding to those source holes can
therefore survive the move. That violates MREMAP_FIXED semantics, where
the destination range is supposed to be unmapped before the move, and it
also means that preserved source gaps do not necessarily become gaps in
the destination. Userspace can then observe mappings at destination
addresses that should have become holes.
Unmap the full fixed target range once before moving the first source
segment in the multi-VMA move path. Split mremap_to() into a wrapper that
performs the MREMAP_FIXED target unmap and a lower-level __mremap_to()
helper that assumes any required target unmap has already been handled.
Then the multi-VMA path can call __mremap_to() after clearing the full
fixed target, while the single-VMA path continues to use mremap_to().
Use a common unmap_fixed_target() helper for both the ordinary mremap_to()
target unmap and the full-range target unmap. This keeps the existing
iterator invalidation and source VMA reload behaviour in one place. The
full target unmap still uses the existing uf_unmap_early path for
userfaultfd unmap notifications.
Extend the multi-VMA mremap selftest to check that the destination pages
corresponding to source holes are unmapped after mremap(). This covers
both MREMAP_FIXED and MREMAP_FIXED |
MREMAP_DONTUNMAP.
Fixes: d23cb648e365 ("mm/mremap: permit mremap() move of multiple VMAs")
Cc: [email protected] # 6.17+
Signed-off-by: fujunjie <[email protected]>
---
mm/mremap.c | 92 +++++++++++++++++-------
tools/testing/selftests/mm/mremap_test.c | 52 ++++++++++++++
2 files changed, 118 insertions(+), 26 deletions(-)
diff --git a/mm/mremap.c b/mm/mremap.c
index e9c8b1d05832..e91fff2ba3ce 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -1430,37 +1430,39 @@ static unsigned long shrink_vma(struct vma_remap_struct
*vrm,
return 0;
}
+static unsigned long unmap_fixed_target(struct vma_remap_struct *vrm,
+ unsigned long addr, unsigned long len)
+{
+ struct mm_struct *mm = current->mm;
+ unsigned long err;
+
+ err = do_munmap(mm, addr, len, vrm->uf_unmap_early);
+ vrm->vma = NULL; /* Invalidated. */
+ vrm->vmi_needs_invalidate = true;
+ if (err)
+ return err;
+
+ /*
+ * If we remap a portion of a VMA elsewhere in the same VMA, this can
+ * invalidate the old VMA. Reset.
+ */
+ vrm->vma = vma_lookup(mm, vrm->addr);
+ if (!vrm->vma)
+ return -EFAULT;
+
+ return 0;
+}
+
/*
- * mremap_to() - remap a vma to a new location.
+ * __mremap_to() - remap a vma to a new location, assuming any required
+ * MREMAP_FIXED target unmap has already been handled.
* Returns: The new address of the vma or an error.
*/
-static unsigned long mremap_to(struct vma_remap_struct *vrm)
+static unsigned long __mremap_to(struct vma_remap_struct *vrm)
{
struct mm_struct *mm = current->mm;
unsigned long err;
- if (vrm->flags & MREMAP_FIXED) {
- /*
- * In mremap_to().
- * VMA is moved to dst address, and munmap dst first.
- * do_munmap will check if dst is sealed.
- */
- err = do_munmap(mm, vrm->new_addr, vrm->new_len,
- vrm->uf_unmap_early);
- vrm->vma = NULL; /* Invalidated. */
- vrm->vmi_needs_invalidate = true;
- if (err)
- return err;
-
- /*
- * If we remap a portion of a VMA elsewhere in the same VMA,
- * this can invalidate the old VMA. Reset.
- */
- vrm->vma = vma_lookup(mm, vrm->addr);
- if (!vrm->vma)
- return -EFAULT;
- }
-
if (vrm->remap_type == MREMAP_SHRINK) {
err = shrink_vma(vrm, /* drop_lock= */false);
if (err)
@@ -1486,6 +1488,23 @@ static unsigned long mremap_to(struct vma_remap_struct
*vrm)
return move_vma(vrm);
}
+/*
+ * mremap_to() - remap a vma to a new location.
+ * Returns: The new address of the vma or an error.
+ */
+static unsigned long mremap_to(struct vma_remap_struct *vrm)
+{
+ unsigned long err;
+
+ if (vrm->flags & MREMAP_FIXED) {
+ err = unmap_fixed_target(vrm, vrm->new_addr, vrm->new_len);
+ if (err)
+ return err;
+ }
+
+ return __mremap_to(vrm);
+}
+
static int vma_expandable(struct vm_area_struct *vma, unsigned long delta)
{
unsigned long end = vma->vm_end + delta;
@@ -1882,9 +1901,11 @@ static unsigned long remap_move(struct vma_remap_struct
*vrm)
unsigned long start = vrm->addr;
unsigned long end = vrm->addr + vrm->old_len;
unsigned long new_addr = vrm->new_addr;
+ unsigned long new_len = vrm->new_len;
unsigned long target_addr = new_addr;
unsigned long res = -EFAULT;
unsigned long last_end;
+ bool fixed_target_unmapped = false;
bool seen_vma = false;
VMA_ITERATOR(vmi, current->mm, start);
@@ -1939,8 +1960,27 @@ static unsigned long remap_move(struct vma_remap_struct
*vrm)
}
res_vma = check_prep_vma(vrm);
- if (!res_vma)
- res_vma = mremap_to(vrm);
+ /*
+ * remap_move() narrows vrm->new_len to the current source VMA
+ * segment before calling mremap_to(). For sparse source ranges
+ * this would leave target ranges corresponding to source holes
+ * mapped.
+ *
+ * If the requested source range extends beyond the first VMA,
+ * unmap the full fixed target range once, using the original
+ * new_addr/new_len, before moving any segment.
+ */
+ if (!res_vma && !seen_vma && vma->vm_end < end) {
+ res_vma = unmap_fixed_target(vrm, new_addr, new_len);
+ if (!res_vma)
+ fixed_target_unmapped = true;
+ }
+ if (!res_vma) {
+ if (fixed_target_unmapped)
+ res_vma = __mremap_to(vrm);
+ else
+ res_vma = mremap_to(vrm);
+ }
if (IS_ERR_VALUE(res_vma))
return res_vma;
diff --git a/tools/testing/selftests/mm/mremap_test.c
b/tools/testing/selftests/mm/mremap_test.c
index 308576437228..ebbe1aa42ef0 100644
--- a/tools/testing/selftests/mm/mremap_test.c
+++ b/tools/testing/selftests/mm/mremap_test.c
@@ -419,6 +419,54 @@ static bool is_multiple_vma_range_ok(unsigned int
pattern_seed,
return true;
}
+static bool is_range_unmapped(void *addr, size_t size)
+{
+ void *ptr;
+
+ ptr = mmap(addr, size, PROT_NONE,
+ MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE, -1, 0);
+ if (ptr == MAP_FAILED) {
+ if (errno == EEXIST)
+ ksft_print_msg("range %p-%p is still mapped\n",
+ addr, (char *)addr + size);
+ else
+ perror("mmap MAP_FIXED_NOREPLACE");
+ return false;
+ }
+
+ if (ptr != addr) {
+ ksft_print_msg("mmap MAP_FIXED_NOREPLACE returned %p, expected
%p\n",
+ ptr, addr);
+ munmap(ptr, size);
+ return false;
+ }
+
+ if (munmap(ptr, size)) {
+ perror("munmap MAP_FIXED_NOREPLACE probe");
+ return false;
+ }
+
+ return true;
+}
+
+static bool multiple_vma_holes_unmapped(char *ptr, unsigned long page_size)
+{
+ static const int holes[] = { 1, 3, 7, 9 };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(holes); i++) {
+ char *addr = &ptr[holes[i] * page_size];
+
+ if (!is_range_unmapped(addr, page_size)) {
+ ksft_print_msg("target hole page %d is mapped\n",
+ holes[i]);
+ return false;
+ }
+ }
+
+ return true;
+}
+
static void mremap_move_multiple_vmas(unsigned int pattern_seed,
unsigned long page_size,
bool dont_unmap)
@@ -529,6 +577,10 @@ static void mremap_move_multiple_vmas(unsigned int
pattern_seed,
success = false;
goto out_unmap;
}
+ if (!multiple_vma_holes_unmapped(tgt_ptr, page_size)) {
+ success = false;
+ goto out_unmap;
+ }
out_unmap:
if (munmap(tgt_ptr, 2 * size))
base-commit: 1b55f8358e35a67bf3969339ea7b86988af92f66
--
2.34.1