mirror of
https://github.com/torvalds/linux.git
synced 2024-12-26 04:42:12 +00:00
dadbd85f2a
When remapping a mapping where a portion of a VMA is remapped
into another portion of the VMA it can cause the VMA to become
split. During the copy_vma operation the VMA can actually
be remerged if it's an anonymous VMA whose pages have not yet
been faulted. This isn't normally a problem because at the end
of the remap the original portion is unmapped causing it to
become split again.
However, MREMAP_DONTUNMAP leaves that original portion in place which
means that the VMA which was split and then remerged is not actually
split at the end of the mremap. This patch fixes a bug where
we don't detect that the VMAs got remerged and we end up
putting back VM_ACCOUNT on the next mapping which is completely
unreleated. When that next mapping is unmapped it results in
incorrectly unaccounting for the memory which was never accounted,
and eventually we will underflow on the memory comittment.
There is also another issue which is similar, we're currently
accouting for the number of pages in the new_vma but that's wrong.
We need to account for the length of the remap operation as that's
all that is being added. If there was a mapping already at that
location its comittment would have been adjusted as part of
the munmap at the start of the mremap.
A really simple repro can be seen in:
https://gist.github.com/bgaff/e101ce99da7d9a8c60acc641d07f312c
Fixes: e346b38130
("mm/mremap: add MREMAP_DONTUNMAP to mremap()")
Reported-by: syzbot <syzkaller@googlegroups.com>
Signed-off-by: Brian Geffon <bgeffon@google.com>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
801 lines
21 KiB
C
801 lines
21 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* mm/mremap.c
|
|
*
|
|
* (C) Copyright 1996 Linus Torvalds
|
|
*
|
|
* Address space accounting code <alan@lxorguk.ukuu.org.uk>
|
|
* (C) Copyright 2002 Red Hat Inc, All Rights Reserved
|
|
*/
|
|
|
|
#include <linux/mm.h>
|
|
#include <linux/hugetlb.h>
|
|
#include <linux/shm.h>
|
|
#include <linux/ksm.h>
|
|
#include <linux/mman.h>
|
|
#include <linux/swap.h>
|
|
#include <linux/capability.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/swapops.h>
|
|
#include <linux/highmem.h>
|
|
#include <linux/security.h>
|
|
#include <linux/syscalls.h>
|
|
#include <linux/mmu_notifier.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/mm-arch-hooks.h>
|
|
#include <linux/userfaultfd_k.h>
|
|
|
|
#include <asm/cacheflush.h>
|
|
#include <asm/tlbflush.h>
|
|
|
|
#include "internal.h"
|
|
|
|
static pmd_t *get_old_pmd(struct mm_struct *mm, unsigned long addr)
|
|
{
|
|
pgd_t *pgd;
|
|
p4d_t *p4d;
|
|
pud_t *pud;
|
|
pmd_t *pmd;
|
|
|
|
pgd = pgd_offset(mm, addr);
|
|
if (pgd_none_or_clear_bad(pgd))
|
|
return NULL;
|
|
|
|
p4d = p4d_offset(pgd, addr);
|
|
if (p4d_none_or_clear_bad(p4d))
|
|
return NULL;
|
|
|
|
pud = pud_offset(p4d, addr);
|
|
if (pud_none_or_clear_bad(pud))
|
|
return NULL;
|
|
|
|
pmd = pmd_offset(pud, addr);
|
|
if (pmd_none(*pmd))
|
|
return NULL;
|
|
|
|
return pmd;
|
|
}
|
|
|
|
static pmd_t *alloc_new_pmd(struct mm_struct *mm, struct vm_area_struct *vma,
|
|
unsigned long addr)
|
|
{
|
|
pgd_t *pgd;
|
|
p4d_t *p4d;
|
|
pud_t *pud;
|
|
pmd_t *pmd;
|
|
|
|
pgd = pgd_offset(mm, addr);
|
|
p4d = p4d_alloc(mm, pgd, addr);
|
|
if (!p4d)
|
|
return NULL;
|
|
pud = pud_alloc(mm, p4d, addr);
|
|
if (!pud)
|
|
return NULL;
|
|
|
|
pmd = pmd_alloc(mm, pud, addr);
|
|
if (!pmd)
|
|
return NULL;
|
|
|
|
VM_BUG_ON(pmd_trans_huge(*pmd));
|
|
|
|
return pmd;
|
|
}
|
|
|
|
static void take_rmap_locks(struct vm_area_struct *vma)
|
|
{
|
|
if (vma->vm_file)
|
|
i_mmap_lock_write(vma->vm_file->f_mapping);
|
|
if (vma->anon_vma)
|
|
anon_vma_lock_write(vma->anon_vma);
|
|
}
|
|
|
|
static void drop_rmap_locks(struct vm_area_struct *vma)
|
|
{
|
|
if (vma->anon_vma)
|
|
anon_vma_unlock_write(vma->anon_vma);
|
|
if (vma->vm_file)
|
|
i_mmap_unlock_write(vma->vm_file->f_mapping);
|
|
}
|
|
|
|
static pte_t move_soft_dirty_pte(pte_t pte)
|
|
{
|
|
/*
|
|
* Set soft dirty bit so we can notice
|
|
* in userspace the ptes were moved.
|
|
*/
|
|
#ifdef CONFIG_MEM_SOFT_DIRTY
|
|
if (pte_present(pte))
|
|
pte = pte_mksoft_dirty(pte);
|
|
else if (is_swap_pte(pte))
|
|
pte = pte_swp_mksoft_dirty(pte);
|
|
#endif
|
|
return pte;
|
|
}
|
|
|
|
static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd,
|
|
unsigned long old_addr, unsigned long old_end,
|
|
struct vm_area_struct *new_vma, pmd_t *new_pmd,
|
|
unsigned long new_addr, bool need_rmap_locks)
|
|
{
|
|
struct mm_struct *mm = vma->vm_mm;
|
|
pte_t *old_pte, *new_pte, pte;
|
|
spinlock_t *old_ptl, *new_ptl;
|
|
bool force_flush = false;
|
|
unsigned long len = old_end - old_addr;
|
|
|
|
/*
|
|
* When need_rmap_locks is true, we take the i_mmap_rwsem and anon_vma
|
|
* locks to ensure that rmap will always observe either the old or the
|
|
* new ptes. This is the easiest way to avoid races with
|
|
* truncate_pagecache(), page migration, etc...
|
|
*
|
|
* When need_rmap_locks is false, we use other ways to avoid
|
|
* such races:
|
|
*
|
|
* - During exec() shift_arg_pages(), we use a specially tagged vma
|
|
* which rmap call sites look for using vma_is_temporary_stack().
|
|
*
|
|
* - During mremap(), new_vma is often known to be placed after vma
|
|
* in rmap traversal order. This ensures rmap will always observe
|
|
* either the old pte, or the new pte, or both (the page table locks
|
|
* serialize access to individual ptes, but only rmap traversal
|
|
* order guarantees that we won't miss both the old and new ptes).
|
|
*/
|
|
if (need_rmap_locks)
|
|
take_rmap_locks(vma);
|
|
|
|
/*
|
|
* We don't have to worry about the ordering of src and dst
|
|
* pte locks because exclusive mmap_sem prevents deadlock.
|
|
*/
|
|
old_pte = pte_offset_map_lock(mm, old_pmd, old_addr, &old_ptl);
|
|
new_pte = pte_offset_map(new_pmd, new_addr);
|
|
new_ptl = pte_lockptr(mm, new_pmd);
|
|
if (new_ptl != old_ptl)
|
|
spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
|
|
flush_tlb_batched_pending(vma->vm_mm);
|
|
arch_enter_lazy_mmu_mode();
|
|
|
|
for (; old_addr < old_end; old_pte++, old_addr += PAGE_SIZE,
|
|
new_pte++, new_addr += PAGE_SIZE) {
|
|
if (pte_none(*old_pte))
|
|
continue;
|
|
|
|
pte = ptep_get_and_clear(mm, old_addr, old_pte);
|
|
/*
|
|
* If we are remapping a valid PTE, make sure
|
|
* to flush TLB before we drop the PTL for the
|
|
* PTE.
|
|
*
|
|
* NOTE! Both old and new PTL matter: the old one
|
|
* for racing with page_mkclean(), the new one to
|
|
* make sure the physical page stays valid until
|
|
* the TLB entry for the old mapping has been
|
|
* flushed.
|
|
*/
|
|
if (pte_present(pte))
|
|
force_flush = true;
|
|
pte = move_pte(pte, new_vma->vm_page_prot, old_addr, new_addr);
|
|
pte = move_soft_dirty_pte(pte);
|
|
set_pte_at(mm, new_addr, new_pte, pte);
|
|
}
|
|
|
|
arch_leave_lazy_mmu_mode();
|
|
if (force_flush)
|
|
flush_tlb_range(vma, old_end - len, old_end);
|
|
if (new_ptl != old_ptl)
|
|
spin_unlock(new_ptl);
|
|
pte_unmap(new_pte - 1);
|
|
pte_unmap_unlock(old_pte - 1, old_ptl);
|
|
if (need_rmap_locks)
|
|
drop_rmap_locks(vma);
|
|
}
|
|
|
|
#ifdef CONFIG_HAVE_MOVE_PMD
|
|
static bool move_normal_pmd(struct vm_area_struct *vma, unsigned long old_addr,
|
|
unsigned long new_addr, unsigned long old_end,
|
|
pmd_t *old_pmd, pmd_t *new_pmd)
|
|
{
|
|
spinlock_t *old_ptl, *new_ptl;
|
|
struct mm_struct *mm = vma->vm_mm;
|
|
pmd_t pmd;
|
|
|
|
if ((old_addr & ~PMD_MASK) || (new_addr & ~PMD_MASK)
|
|
|| old_end - old_addr < PMD_SIZE)
|
|
return false;
|
|
|
|
/*
|
|
* The destination pmd shouldn't be established, free_pgtables()
|
|
* should have release it.
|
|
*/
|
|
if (WARN_ON(!pmd_none(*new_pmd)))
|
|
return false;
|
|
|
|
/*
|
|
* We don't have to worry about the ordering of src and dst
|
|
* ptlocks because exclusive mmap_sem prevents deadlock.
|
|
*/
|
|
old_ptl = pmd_lock(vma->vm_mm, old_pmd);
|
|
new_ptl = pmd_lockptr(mm, new_pmd);
|
|
if (new_ptl != old_ptl)
|
|
spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
|
|
|
|
/* Clear the pmd */
|
|
pmd = *old_pmd;
|
|
pmd_clear(old_pmd);
|
|
|
|
VM_BUG_ON(!pmd_none(*new_pmd));
|
|
|
|
/* Set the new pmd */
|
|
set_pmd_at(mm, new_addr, new_pmd, pmd);
|
|
flush_tlb_range(vma, old_addr, old_addr + PMD_SIZE);
|
|
if (new_ptl != old_ptl)
|
|
spin_unlock(new_ptl);
|
|
spin_unlock(old_ptl);
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
unsigned long move_page_tables(struct vm_area_struct *vma,
|
|
unsigned long old_addr, struct vm_area_struct *new_vma,
|
|
unsigned long new_addr, unsigned long len,
|
|
bool need_rmap_locks)
|
|
{
|
|
unsigned long extent, next, old_end;
|
|
struct mmu_notifier_range range;
|
|
pmd_t *old_pmd, *new_pmd;
|
|
|
|
old_end = old_addr + len;
|
|
flush_cache_range(vma, old_addr, old_end);
|
|
|
|
mmu_notifier_range_init(&range, MMU_NOTIFY_UNMAP, 0, vma, vma->vm_mm,
|
|
old_addr, old_end);
|
|
mmu_notifier_invalidate_range_start(&range);
|
|
|
|
for (; old_addr < old_end; old_addr += extent, new_addr += extent) {
|
|
cond_resched();
|
|
next = (old_addr + PMD_SIZE) & PMD_MASK;
|
|
/* even if next overflowed, extent below will be ok */
|
|
extent = next - old_addr;
|
|
if (extent > old_end - old_addr)
|
|
extent = old_end - old_addr;
|
|
old_pmd = get_old_pmd(vma->vm_mm, old_addr);
|
|
if (!old_pmd)
|
|
continue;
|
|
new_pmd = alloc_new_pmd(vma->vm_mm, vma, new_addr);
|
|
if (!new_pmd)
|
|
break;
|
|
if (is_swap_pmd(*old_pmd) || pmd_trans_huge(*old_pmd)) {
|
|
if (extent == HPAGE_PMD_SIZE) {
|
|
bool moved;
|
|
/* See comment in move_ptes() */
|
|
if (need_rmap_locks)
|
|
take_rmap_locks(vma);
|
|
moved = move_huge_pmd(vma, old_addr, new_addr,
|
|
old_end, old_pmd, new_pmd);
|
|
if (need_rmap_locks)
|
|
drop_rmap_locks(vma);
|
|
if (moved)
|
|
continue;
|
|
}
|
|
split_huge_pmd(vma, old_pmd, old_addr);
|
|
if (pmd_trans_unstable(old_pmd))
|
|
continue;
|
|
} else if (extent == PMD_SIZE) {
|
|
#ifdef CONFIG_HAVE_MOVE_PMD
|
|
/*
|
|
* If the extent is PMD-sized, try to speed the move by
|
|
* moving at the PMD level if possible.
|
|
*/
|
|
bool moved;
|
|
|
|
if (need_rmap_locks)
|
|
take_rmap_locks(vma);
|
|
moved = move_normal_pmd(vma, old_addr, new_addr,
|
|
old_end, old_pmd, new_pmd);
|
|
if (need_rmap_locks)
|
|
drop_rmap_locks(vma);
|
|
if (moved)
|
|
continue;
|
|
#endif
|
|
}
|
|
|
|
if (pte_alloc(new_vma->vm_mm, new_pmd))
|
|
break;
|
|
next = (new_addr + PMD_SIZE) & PMD_MASK;
|
|
if (extent > next - new_addr)
|
|
extent = next - new_addr;
|
|
move_ptes(vma, old_pmd, old_addr, old_addr + extent, new_vma,
|
|
new_pmd, new_addr, need_rmap_locks);
|
|
}
|
|
|
|
mmu_notifier_invalidate_range_end(&range);
|
|
|
|
return len + old_addr - old_end; /* how much done */
|
|
}
|
|
|
|
static unsigned long move_vma(struct vm_area_struct *vma,
|
|
unsigned long old_addr, unsigned long old_len,
|
|
unsigned long new_len, unsigned long new_addr,
|
|
bool *locked, unsigned long flags,
|
|
struct vm_userfaultfd_ctx *uf, struct list_head *uf_unmap)
|
|
{
|
|
struct mm_struct *mm = vma->vm_mm;
|
|
struct vm_area_struct *new_vma;
|
|
unsigned long vm_flags = vma->vm_flags;
|
|
unsigned long new_pgoff;
|
|
unsigned long moved_len;
|
|
unsigned long excess = 0;
|
|
unsigned long hiwater_vm;
|
|
int split = 0;
|
|
int err;
|
|
bool need_rmap_locks;
|
|
|
|
/*
|
|
* We'd prefer to avoid failure later on in do_munmap:
|
|
* which may split one vma into three before unmapping.
|
|
*/
|
|
if (mm->map_count >= sysctl_max_map_count - 3)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* Advise KSM to break any KSM pages in the area to be moved:
|
|
* it would be confusing if they were to turn up at the new
|
|
* location, where they happen to coincide with different KSM
|
|
* pages recently unmapped. But leave vma->vm_flags as it was,
|
|
* so KSM can come around to merge on vma and new_vma afterwards.
|
|
*/
|
|
err = ksm_madvise(vma, old_addr, old_addr + old_len,
|
|
MADV_UNMERGEABLE, &vm_flags);
|
|
if (err)
|
|
return err;
|
|
|
|
new_pgoff = vma->vm_pgoff + ((old_addr - vma->vm_start) >> PAGE_SHIFT);
|
|
new_vma = copy_vma(&vma, new_addr, new_len, new_pgoff,
|
|
&need_rmap_locks);
|
|
if (!new_vma)
|
|
return -ENOMEM;
|
|
|
|
moved_len = move_page_tables(vma, old_addr, new_vma, new_addr, old_len,
|
|
need_rmap_locks);
|
|
if (moved_len < old_len) {
|
|
err = -ENOMEM;
|
|
} else if (vma->vm_ops && vma->vm_ops->mremap) {
|
|
err = vma->vm_ops->mremap(new_vma);
|
|
}
|
|
|
|
if (unlikely(err)) {
|
|
/*
|
|
* On error, move entries back from new area to old,
|
|
* which will succeed since page tables still there,
|
|
* and then proceed to unmap new area instead of old.
|
|
*/
|
|
move_page_tables(new_vma, new_addr, vma, old_addr, moved_len,
|
|
true);
|
|
vma = new_vma;
|
|
old_len = new_len;
|
|
old_addr = new_addr;
|
|
new_addr = err;
|
|
} else {
|
|
mremap_userfaultfd_prep(new_vma, uf);
|
|
arch_remap(mm, old_addr, old_addr + old_len,
|
|
new_addr, new_addr + new_len);
|
|
}
|
|
|
|
/* Conceal VM_ACCOUNT so old reservation is not undone */
|
|
if (vm_flags & VM_ACCOUNT) {
|
|
vma->vm_flags &= ~VM_ACCOUNT;
|
|
excess = vma->vm_end - vma->vm_start - old_len;
|
|
if (old_addr > vma->vm_start &&
|
|
old_addr + old_len < vma->vm_end)
|
|
split = 1;
|
|
}
|
|
|
|
/*
|
|
* If we failed to move page tables we still do total_vm increment
|
|
* since do_munmap() will decrement it by old_len == new_len.
|
|
*
|
|
* Since total_vm is about to be raised artificially high for a
|
|
* moment, we need to restore high watermark afterwards: if stats
|
|
* are taken meanwhile, total_vm and hiwater_vm appear too high.
|
|
* If this were a serious issue, we'd add a flag to do_munmap().
|
|
*/
|
|
hiwater_vm = mm->hiwater_vm;
|
|
vm_stat_account(mm, vma->vm_flags, new_len >> PAGE_SHIFT);
|
|
|
|
/* Tell pfnmap has moved from this vma */
|
|
if (unlikely(vma->vm_flags & VM_PFNMAP))
|
|
untrack_pfn_moved(vma);
|
|
|
|
if (unlikely(!err && (flags & MREMAP_DONTUNMAP))) {
|
|
if (vm_flags & VM_ACCOUNT) {
|
|
/* Always put back VM_ACCOUNT since we won't unmap */
|
|
vma->vm_flags |= VM_ACCOUNT;
|
|
|
|
vm_acct_memory(new_len >> PAGE_SHIFT);
|
|
}
|
|
|
|
/*
|
|
* VMAs can actually be merged back together in copy_vma
|
|
* calling merge_vma. This can happen with anonymous vmas
|
|
* which have not yet been faulted, so if we were to consider
|
|
* this VMA split we'll end up adding VM_ACCOUNT on the
|
|
* next VMA, which is completely unrelated if this VMA
|
|
* was re-merged.
|
|
*/
|
|
if (split && new_vma == vma)
|
|
split = 0;
|
|
|
|
/* We always clear VM_LOCKED[ONFAULT] on the old vma */
|
|
vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
|
|
|
|
/* Because we won't unmap we don't need to touch locked_vm */
|
|
goto out;
|
|
}
|
|
|
|
if (do_munmap(mm, old_addr, old_len, uf_unmap) < 0) {
|
|
/* OOM: unable to split vma, just get accounts right */
|
|
vm_unacct_memory(excess >> PAGE_SHIFT);
|
|
excess = 0;
|
|
}
|
|
|
|
if (vm_flags & VM_LOCKED) {
|
|
mm->locked_vm += new_len >> PAGE_SHIFT;
|
|
*locked = true;
|
|
}
|
|
out:
|
|
mm->hiwater_vm = hiwater_vm;
|
|
|
|
/* Restore VM_ACCOUNT if one or two pieces of vma left */
|
|
if (excess) {
|
|
vma->vm_flags |= VM_ACCOUNT;
|
|
if (split)
|
|
vma->vm_next->vm_flags |= VM_ACCOUNT;
|
|
}
|
|
|
|
return new_addr;
|
|
}
|
|
|
|
static struct vm_area_struct *vma_to_resize(unsigned long addr,
|
|
unsigned long old_len, unsigned long new_len, unsigned long flags,
|
|
unsigned long *p)
|
|
{
|
|
struct mm_struct *mm = current->mm;
|
|
struct vm_area_struct *vma = find_vma(mm, addr);
|
|
unsigned long pgoff;
|
|
|
|
if (!vma || vma->vm_start > addr)
|
|
return ERR_PTR(-EFAULT);
|
|
|
|
/*
|
|
* !old_len is a special case where an attempt is made to 'duplicate'
|
|
* a mapping. This makes no sense for private mappings as it will
|
|
* instead create a fresh/new mapping unrelated to the original. This
|
|
* is contrary to the basic idea of mremap which creates new mappings
|
|
* based on the original. There are no known use cases for this
|
|
* behavior. As a result, fail such attempts.
|
|
*/
|
|
if (!old_len && !(vma->vm_flags & (VM_SHARED | VM_MAYSHARE))) {
|
|
pr_warn_once("%s (%d): attempted to duplicate a private mapping with mremap. This is not supported.\n", current->comm, current->pid);
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
|
|
if (flags & MREMAP_DONTUNMAP && (!vma_is_anonymous(vma) ||
|
|
vma->vm_flags & VM_SHARED))
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
if (is_vm_hugetlb_page(vma))
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
/* We can't remap across vm area boundaries */
|
|
if (old_len > vma->vm_end - addr)
|
|
return ERR_PTR(-EFAULT);
|
|
|
|
if (new_len == old_len)
|
|
return vma;
|
|
|
|
/* Need to be careful about a growing mapping */
|
|
pgoff = (addr - vma->vm_start) >> PAGE_SHIFT;
|
|
pgoff += vma->vm_pgoff;
|
|
if (pgoff + (new_len >> PAGE_SHIFT) < pgoff)
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
if (vma->vm_flags & (VM_DONTEXPAND | VM_PFNMAP))
|
|
return ERR_PTR(-EFAULT);
|
|
|
|
if (vma->vm_flags & VM_LOCKED) {
|
|
unsigned long locked, lock_limit;
|
|
locked = mm->locked_vm << PAGE_SHIFT;
|
|
lock_limit = rlimit(RLIMIT_MEMLOCK);
|
|
locked += new_len - old_len;
|
|
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
|
|
return ERR_PTR(-EAGAIN);
|
|
}
|
|
|
|
if (!may_expand_vm(mm, vma->vm_flags,
|
|
(new_len - old_len) >> PAGE_SHIFT))
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
if (vma->vm_flags & VM_ACCOUNT) {
|
|
unsigned long charged = (new_len - old_len) >> PAGE_SHIFT;
|
|
if (security_vm_enough_memory_mm(mm, charged))
|
|
return ERR_PTR(-ENOMEM);
|
|
*p = charged;
|
|
}
|
|
|
|
return vma;
|
|
}
|
|
|
|
static unsigned long mremap_to(unsigned long addr, unsigned long old_len,
|
|
unsigned long new_addr, unsigned long new_len, bool *locked,
|
|
unsigned long flags, struct vm_userfaultfd_ctx *uf,
|
|
struct list_head *uf_unmap_early,
|
|
struct list_head *uf_unmap)
|
|
{
|
|
struct mm_struct *mm = current->mm;
|
|
struct vm_area_struct *vma;
|
|
unsigned long ret = -EINVAL;
|
|
unsigned long charged = 0;
|
|
unsigned long map_flags = 0;
|
|
|
|
if (offset_in_page(new_addr))
|
|
goto out;
|
|
|
|
if (new_len > TASK_SIZE || new_addr > TASK_SIZE - new_len)
|
|
goto out;
|
|
|
|
/* Ensure the old/new locations do not overlap */
|
|
if (addr + old_len > new_addr && new_addr + new_len > addr)
|
|
goto out;
|
|
|
|
/*
|
|
* move_vma() need us to stay 4 maps below the threshold, otherwise
|
|
* it will bail out at the very beginning.
|
|
* That is a problem if we have already unmaped the regions here
|
|
* (new_addr, and old_addr), because userspace will not know the
|
|
* state of the vma's after it gets -ENOMEM.
|
|
* So, to avoid such scenario we can pre-compute if the whole
|
|
* operation has high chances to success map-wise.
|
|
* Worst-scenario case is when both vma's (new_addr and old_addr) get
|
|
* split in 3 before unmaping it.
|
|
* That means 2 more maps (1 for each) to the ones we already hold.
|
|
* Check whether current map count plus 2 still leads us to 4 maps below
|
|
* the threshold, otherwise return -ENOMEM here to be more safe.
|
|
*/
|
|
if ((mm->map_count + 2) >= sysctl_max_map_count - 3)
|
|
return -ENOMEM;
|
|
|
|
if (flags & MREMAP_FIXED) {
|
|
ret = do_munmap(mm, new_addr, new_len, uf_unmap_early);
|
|
if (ret)
|
|
goto out;
|
|
}
|
|
|
|
if (old_len >= new_len) {
|
|
ret = do_munmap(mm, addr+new_len, old_len - new_len, uf_unmap);
|
|
if (ret && old_len != new_len)
|
|
goto out;
|
|
old_len = new_len;
|
|
}
|
|
|
|
vma = vma_to_resize(addr, old_len, new_len, flags, &charged);
|
|
if (IS_ERR(vma)) {
|
|
ret = PTR_ERR(vma);
|
|
goto out;
|
|
}
|
|
|
|
/* MREMAP_DONTUNMAP expands by old_len since old_len == new_len */
|
|
if (flags & MREMAP_DONTUNMAP &&
|
|
!may_expand_vm(mm, vma->vm_flags, old_len >> PAGE_SHIFT)) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
if (flags & MREMAP_FIXED)
|
|
map_flags |= MAP_FIXED;
|
|
|
|
if (vma->vm_flags & VM_MAYSHARE)
|
|
map_flags |= MAP_SHARED;
|
|
|
|
ret = get_unmapped_area(vma->vm_file, new_addr, new_len, vma->vm_pgoff +
|
|
((addr - vma->vm_start) >> PAGE_SHIFT),
|
|
map_flags);
|
|
if (IS_ERR_VALUE(ret))
|
|
goto out1;
|
|
|
|
/* We got a new mapping */
|
|
if (!(flags & MREMAP_FIXED))
|
|
new_addr = ret;
|
|
|
|
ret = move_vma(vma, addr, old_len, new_len, new_addr, locked, flags, uf,
|
|
uf_unmap);
|
|
|
|
if (!(offset_in_page(ret)))
|
|
goto out;
|
|
|
|
out1:
|
|
vm_unacct_memory(charged);
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static int vma_expandable(struct vm_area_struct *vma, unsigned long delta)
|
|
{
|
|
unsigned long end = vma->vm_end + delta;
|
|
if (end < vma->vm_end) /* overflow */
|
|
return 0;
|
|
if (vma->vm_next && vma->vm_next->vm_start < end) /* intersection */
|
|
return 0;
|
|
if (get_unmapped_area(NULL, vma->vm_start, end - vma->vm_start,
|
|
0, MAP_FIXED) & ~PAGE_MASK)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Expand (or shrink) an existing mapping, potentially moving it at the
|
|
* same time (controlled by the MREMAP_MAYMOVE flag and available VM space)
|
|
*
|
|
* MREMAP_FIXED option added 5-Dec-1999 by Benjamin LaHaise
|
|
* This option implies MREMAP_MAYMOVE.
|
|
*/
|
|
SYSCALL_DEFINE5(mremap, unsigned long, addr, unsigned long, old_len,
|
|
unsigned long, new_len, unsigned long, flags,
|
|
unsigned long, new_addr)
|
|
{
|
|
struct mm_struct *mm = current->mm;
|
|
struct vm_area_struct *vma;
|
|
unsigned long ret = -EINVAL;
|
|
unsigned long charged = 0;
|
|
bool locked = false;
|
|
bool downgraded = false;
|
|
struct vm_userfaultfd_ctx uf = NULL_VM_UFFD_CTX;
|
|
LIST_HEAD(uf_unmap_early);
|
|
LIST_HEAD(uf_unmap);
|
|
|
|
/*
|
|
* There is a deliberate asymmetry here: we strip the pointer tag
|
|
* from the old address but leave the new address alone. This is
|
|
* for consistency with mmap(), where we prevent the creation of
|
|
* aliasing mappings in userspace by leaving the tag bits of the
|
|
* mapping address intact. A non-zero tag will cause the subsequent
|
|
* range checks to reject the address as invalid.
|
|
*
|
|
* See Documentation/arm64/tagged-address-abi.rst for more information.
|
|
*/
|
|
addr = untagged_addr(addr);
|
|
|
|
if (flags & ~(MREMAP_FIXED | MREMAP_MAYMOVE | MREMAP_DONTUNMAP))
|
|
return ret;
|
|
|
|
if (flags & MREMAP_FIXED && !(flags & MREMAP_MAYMOVE))
|
|
return ret;
|
|
|
|
/*
|
|
* MREMAP_DONTUNMAP is always a move and it does not allow resizing
|
|
* in the process.
|
|
*/
|
|
if (flags & MREMAP_DONTUNMAP &&
|
|
(!(flags & MREMAP_MAYMOVE) || old_len != new_len))
|
|
return ret;
|
|
|
|
|
|
if (offset_in_page(addr))
|
|
return ret;
|
|
|
|
old_len = PAGE_ALIGN(old_len);
|
|
new_len = PAGE_ALIGN(new_len);
|
|
|
|
/*
|
|
* We allow a zero old-len as a special case
|
|
* for DOS-emu "duplicate shm area" thing. But
|
|
* a zero new-len is nonsensical.
|
|
*/
|
|
if (!new_len)
|
|
return ret;
|
|
|
|
if (down_write_killable(¤t->mm->mmap_sem))
|
|
return -EINTR;
|
|
|
|
if (flags & (MREMAP_FIXED | MREMAP_DONTUNMAP)) {
|
|
ret = mremap_to(addr, old_len, new_addr, new_len,
|
|
&locked, flags, &uf, &uf_unmap_early,
|
|
&uf_unmap);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Always allow a shrinking remap: that just unmaps
|
|
* the unnecessary pages..
|
|
* __do_munmap does all the needed commit accounting, and
|
|
* downgrades mmap_sem to read if so directed.
|
|
*/
|
|
if (old_len >= new_len) {
|
|
int retval;
|
|
|
|
retval = __do_munmap(mm, addr+new_len, old_len - new_len,
|
|
&uf_unmap, true);
|
|
if (retval < 0 && old_len != new_len) {
|
|
ret = retval;
|
|
goto out;
|
|
/* Returning 1 indicates mmap_sem is downgraded to read. */
|
|
} else if (retval == 1)
|
|
downgraded = true;
|
|
ret = addr;
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Ok, we need to grow..
|
|
*/
|
|
vma = vma_to_resize(addr, old_len, new_len, flags, &charged);
|
|
if (IS_ERR(vma)) {
|
|
ret = PTR_ERR(vma);
|
|
goto out;
|
|
}
|
|
|
|
/* old_len exactly to the end of the area..
|
|
*/
|
|
if (old_len == vma->vm_end - addr) {
|
|
/* can we just expand the current mapping? */
|
|
if (vma_expandable(vma, new_len - old_len)) {
|
|
int pages = (new_len - old_len) >> PAGE_SHIFT;
|
|
|
|
if (vma_adjust(vma, vma->vm_start, addr + new_len,
|
|
vma->vm_pgoff, NULL)) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
vm_stat_account(mm, vma->vm_flags, pages);
|
|
if (vma->vm_flags & VM_LOCKED) {
|
|
mm->locked_vm += pages;
|
|
locked = true;
|
|
new_addr = addr;
|
|
}
|
|
ret = addr;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We weren't able to just expand or shrink the area,
|
|
* we need to create a new one and move it..
|
|
*/
|
|
ret = -ENOMEM;
|
|
if (flags & MREMAP_MAYMOVE) {
|
|
unsigned long map_flags = 0;
|
|
if (vma->vm_flags & VM_MAYSHARE)
|
|
map_flags |= MAP_SHARED;
|
|
|
|
new_addr = get_unmapped_area(vma->vm_file, 0, new_len,
|
|
vma->vm_pgoff +
|
|
((addr - vma->vm_start) >> PAGE_SHIFT),
|
|
map_flags);
|
|
if (IS_ERR_VALUE(new_addr)) {
|
|
ret = new_addr;
|
|
goto out;
|
|
}
|
|
|
|
ret = move_vma(vma, addr, old_len, new_len, new_addr,
|
|
&locked, flags, &uf, &uf_unmap);
|
|
}
|
|
out:
|
|
if (offset_in_page(ret)) {
|
|
vm_unacct_memory(charged);
|
|
locked = 0;
|
|
}
|
|
if (downgraded)
|
|
up_read(¤t->mm->mmap_sem);
|
|
else
|
|
up_write(¤t->mm->mmap_sem);
|
|
if (locked && new_len > old_len)
|
|
mm_populate(new_addr + old_len, new_len - old_len);
|
|
userfaultfd_unmap_complete(mm, &uf_unmap_early);
|
|
mremap_userfaultfd_complete(&uf, addr, new_addr, old_len);
|
|
userfaultfd_unmap_complete(mm, &uf_unmap);
|
|
return ret;
|
|
}
|