[PATCH 41/46] mm/vma: call ->may_split() before a partial merge
From: David Hildenbrand (Arm)
Date: Mon Oct 26 2026 - 19:37:50 EST
From: Chris Mason <mason@xxxxxxxxxx>
A driver can use the ->may_split() hook to forbid a VMA boundary at
certain addresses.
A device-dax device is a character device that gives raw access to a
range of memory through mmap(). device-dax refuses any address that is not
a multiple of the device's alignment through vm_ops->may_split.
vma_merge_existing_range() can move boundaries, but does not call
vm_ops->may_split.
If a range at the edge of a VMA gets the attributes of the adjacent VMA,
the function moves only that range over to the neighbour: one VMA shrinks
and the other grows. This is a partial merge, and vma_modify() tries it
before any split.
Take a device-dax VMA that lies next to another VMA of the same file.
The user calls mprotect() on the one page of the first VMA that touches
the second VMA, and asks for the protection that the second VMA already
has. The result is a partial merge. It puts the boundary inside a 1 GiB
mapping, which is a single leaf entry at the PUD level.
As part of the merge, the kernel looks for a huge PMD to split at the
new boundary. That code does not expect a PUD leaf:
vma_merge_existing_range()
commit_merge()
vma_adjust_trans_huge()
split_huge_pmd_address()
mm_find_pmd()
treats the PUD leaf as a pointer to a PMD table,
so the "table" is the device memory that the leaf maps
__split_huge_pmd()
reads a PMD that the user wrote to the device memory
folio_put() on the folio that this PMD names
Bad. While the code must at some point be taught to split PUD entries as
well, the issue here is that we move VMA boundaries such that they are
incompatible with the driver.
Fix this by calling ->may_split() on the address that a partial merge
would move the boundary to, and by abandoning the merge if the driver
refuses. vma_modify() then tries the split, which fails with -EINVAL.
Factor out existing split handling into check_split() and reuse it.
Fix generated by an LLM and sent by Chris Mason in private.
Fixes: 38607c62b34b ("fs/dax: properly refcount fs dax pages")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Chris Mason <mason@xxxxxxxxxx>
Reviewed-by: Lorenzo Stoakes (ARM) <ljs@xxxxxxxxxx>
[ david: improve description; cleanup the code ]
Signed-off-by: David Hildenbrand (Arm) <david@xxxxxxxxxx>
---
mm/vma.c | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/mm/vma.c b/mm/vma.c
index 1be613044b4a5..acdf00346cda5 100644
--- a/mm/vma.c
+++ b/mm/vma.c
@@ -516,6 +516,15 @@ void unmap_region(struct unmap_desc *unmap)
tlb_finish_mmu(&tlb);
}
+static int check_split(struct vm_area_struct *vma, unsigned long addr)
+{
+ if (!vma->vm_ops)
+ return 0;
+ if (!vma->vm_ops->may_split)
+ return 0;
+ return vma->vm_ops->may_split(vma, addr);
+}
+
/*
* __split_vma() bypasses sysctl_max_map_count checking. We use this where it
* has already been checked or doesn't make sense to fail.
@@ -532,11 +541,9 @@ __split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma,
WARN_ON(vma->vm_start >= addr);
WARN_ON(vma->vm_end <= addr);
- if (vma->vm_ops && vma->vm_ops->may_split) {
- err = vma->vm_ops->may_split(vma, addr);
- if (err)
- return err;
- }
+ err = check_split(vma, addr);
+ if (err)
+ return err;
new = vm_area_dup(vma);
if (!new)
@@ -948,6 +955,10 @@ static __must_check struct vm_area_struct *vma_merge_existing_range(
if (vmg->__remove_middle && !can_merge_remove_vma(middle))
return NULL;
+ /* If middle isn't removed then it's resized, i.e. splits. */
+ if (!vmg->__remove_middle && check_split(middle, left_side ? end : start))
+ return NULL;
+
/*
* If we merge both VMAs, then next is also deleted. This implies
* merge_will_delete_vma also.
--
2.43.0