buffer가 준비됐다고 이후 접근 조건까지 고정된 것은 아닙니다. reclaim에서 보호할지, read/write/execute를 허용할지, 주소와 크기를 바꿀지는 서로 다른 API가 담당합니다. 세 API를 나누어 보면 VMA metadata와 실제 backing의 차이도 명확해집니다.
mlock은 호출 process의 residency 정책입니다
mlock()은 지정한 범위를 reclaim 대상에서 보호하며 기본적으로 nonresident 부분의 population도 요청합니다. mlock2(MLOCK_ONFAULT)는 아직 없는 page를 당장 만들지 않고 fault 시점에 locking하는 정책입니다. mlockall()의 CURRENT와 FUTURE는 호출 process의 현재·미래 mapping에 적용되며 system 전체 memory를 lock하지 않습니다. ONFAULT는 이 정책들과 조합합니다.
ONFAULT도 이미 resident한 page의 locking까지 미루지는 않습니다. 아직 없는 page의 population을 미루는 것이며, MCL_ONFAULT만 단독으로 지정할 수 없고 CURRENT·FUTURE 중 하나 이상과 조합해야 합니다. Linux의 mlock·munlock은 시작 주소를 아래 page 경계로 내리고 원래 범위가 닿은 마지막 page까지 포함합니다. 반면 mprotect는 시작 주소 자체가 page-aligned여야 하므로 두 API의 정렬 규칙을 그대로 바꾸어 쓸 수 없습니다.
kernel은 권한과 RLIMIT_MEMLOCK, start·length 정렬 및 overflow를 검사하고 이미 locked된 범위를 감안합니다. CAP_IPC_LOCK은 limit override와 관계합니다. VMA flags의 변경은 split·merge를 요구할 수 있고 실제 page의 mlocked·unevictable 상태도 맞춰야 합니다. munlock은 이 정책을 제거할 뿐 다른 reference가 있는 folio를 즉시 free하지 않습니다.
unevictable은 kernel version과 configuration의 LRU 구현에 따라 물리적인 list와 통계 표현이 달라질 수 있습니다. 일반 reclaim에서 제외한다는 정책과 “언제나 실제 list node가 없다”는 내부 구현 주장을 구별해야 합니다. mlock은 DMA pin도 아니며 physical location을 device에 약속하지 않습니다.
mprotect는 VMA flags만 바꾸지 않습니다
mprotect()는 page-aligned 범위의 접근 권한을 변경합니다. MAY 권한과 security·architecture 조건을 검사하며 필요한 VMA split·merge를 수행합니다. 현재 PTE의 permission과 TLB도 새 권한에 맞춰야 하므로 vm_flags 대입만으로 끝나지 않습니다.
read-only PTE가 있어도 CoW 또는 write notification으로 해결할 수 있었던 것은 VMA의 write 허용이 남아 있었기 때문입니다. mprotect로 실제 write 허용을 제거하면 같은 fault를 CoW로 해결할 수 없습니다. READ·WRITE·EXEC의 관계는 architecture에 따라 다릅니다.
mremap은 object contents보다 주소를 먼저 바꿉니다
mremap()은 기존 mapping을 줄이거나 늘리고 필요하면 옮깁니다. 기본적으로 현재 자리에서 확장할 수 없으면 실패하며 MAYMOVE가 relocation을 허용합니다. 성공 후 반환 주소를 사용해야 하고 기존 pointer는 무효가 될 수 있습니다. FIXED는 MAYMOVE와 함께 목표 주소를 지정하며 그곳 mapping을 제거할 수 있으므로 특히 주의해야 합니다.
anonymous rmap의 logical vm_pgoff를 유지하면 VA가 바뀌어도 모든 folio index를 다시 쓰지 않아도 됩니다. locked mapping을 확장하면 memlock 한계도 관계합니다. 지원하는 source VMA 범위와 relocation 조합은 kernel version에 따라 확장되어 왔으므로 오래된 단일-VMA 제약을 모든 최신 호출에 일반화하지 않습니다.
DONTUNMAP은 contents를 두 번 소유하는 copy가 아닙니다
MREMAP_DONTUNMAP은 source VMA를 남기지만 원래 PTE의 backing을 그대로 두 주소에서 private copy로 보장하는 API가 아닙니다. 이동 뒤 source 접근은 다시 fault하며 userfaultfd가 있으면 그 protocol이 관여합니다. private anonymous의 일반 refault는 zero-fill이지만 shared shmem처럼 backing이 남아 있으면 다시 얻는 data의 의미도 다릅니다.
아래에서 확인하는 v6.0 interface도 과거의 private-anonymous-only 설명보다 넓은 mapping을 지원하되 여전히 제외 조건과 동일 길이 등 제약이 있습니다. “source가 즉시 zero page로 모두 바뀐다”보다 “source의 translation을 다시 해결한다”가 정확한 관점입니다.
Locking flag를 바꾸는 전체 경로
아래 official Linux v6.0 source는 syscall macro의 body까지 통째로 포함합니다. Generated syscall entry의 machine code나 standalone program 전체라는 뜻은 아닙니다. mlock2(flags=0)는 같은 flags를 do_mlock()에 넘기고 ONFAULT만 지연 locking을 더합니다. munlock()은 범위를 정렬하고 write lock 아래 locking flags를 제거합니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:615-618
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len)
{
return do_mlock(start, len, VM_LOCKED);
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:620-631
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
SYSCALL_DEFINE3(mlock2, unsigned long, start, size_t, len, int, flags)
{
vm_flags_t vm_flags = VM_LOCKED;
if (flags & ~MLOCK_ONFAULT)
return -EINVAL;
if (flags & MLOCK_ONFAULT)
vm_flags |= VM_LOCKONFAULT;
return do_mlock(start, len, vm_flags);
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:633-648
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
SYSCALL_DEFINE2(munlock, unsigned long, start, size_t, len)
{
int ret;
start = untagged_addr(start);
len = PAGE_ALIGN(len + (offset_in_page(start)));
start &= PAGE_MASK;
if (mmap_write_lock_killable(current->mm))
return -EINTR;
ret = apply_vma_lock_flags(start, len, 0);
mmap_write_unlock(current->mm);
return ret;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:568-613
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static __must_check int do_mlock(unsigned long start, size_t len, vm_flags_t flags)
{
unsigned long locked;
unsigned long lock_limit;
int error = -ENOMEM;
start = untagged_addr(start);
if (!can_do_mlock())
return -EPERM;
len = PAGE_ALIGN(len + (offset_in_page(start)));
start &= PAGE_MASK;
lock_limit = rlimit(RLIMIT_MEMLOCK);
lock_limit >>= PAGE_SHIFT;
locked = len >> PAGE_SHIFT;
if (mmap_write_lock_killable(current->mm))
return -EINTR;
locked += current->mm->locked_vm;
if ((locked > lock_limit) && (!capable(CAP_IPC_LOCK))) {
/*
* It is possible that the regions requested intersect with
* previously mlocked areas, that part area in "mm->locked_vm"
* should not be counted to new mlock increment count. So check
* and adjust locked count if necessary.
*/
locked -= count_mm_mlocked_page_nr(current->mm,
start, len);
}
/* check against resource limits */
if ((locked <= lock_limit) || capable(CAP_IPC_LOCK))
error = apply_vma_lock_flags(start, len, flags);
mmap_write_unlock(current->mm);
if (error)
return error;
error = __mm_populate(start, len, 0);
if (error)
return __mlock_posix_error_return(error);
return 0;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:40-47
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
bool can_do_mlock(void)
{
if (rlimit(RLIMIT_MEMLOCK) != 0)
return true;
if (capable(CAP_IPC_LOCK))
return true;
return false;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/mlock.c
*
* (C) Copyright 1995 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/mlock.c:468-515
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int apply_vma_lock_flags(unsigned long start, size_t len,
vm_flags_t flags)
{
unsigned long nstart, end, tmp;
struct vm_area_struct *vma, *prev;
int error;
VM_BUG_ON(offset_in_page(start));
VM_BUG_ON(len != PAGE_ALIGN(len));
end = start + len;
if (end < start)
return -EINVAL;
if (end == start)
return 0;
vma = find_vma(current->mm, start);
if (!vma || vma->vm_start > start)
return -ENOMEM;
prev = vma->vm_prev;
if (start > vma->vm_start)
prev = vma;
for (nstart = start ; ; ) {
vm_flags_t newflags = vma->vm_flags & VM_LOCKED_CLEAR_MASK;
newflags |= flags;
/* Here we know that vma->vm_start <= nstart < vma->vm_end. */
tmp = vma->vm_end;
if (tmp > end)
tmp = end;
error = mlock_fixup(vma, &prev, nstart, tmp, newflags);
if (error)
break;
nstart = tmp;
if (nstart < prev->vm_end)
nstart = prev->vm_end;
if (nstart >= end)
break;
vma = prev->vm_next;
if (!vma || vma->vm_start != nstart) {
error = -ENOMEM;
break;
}
}
return error;
}
can_do_mlock()이 true라고 요청한 양 전체가 허용된 것은 아닙니다. 뒤에서 RLIMIT와 기존 locked 범위의 overlap을 계산합니다. VMA flags 반영 뒤 write lock을 놓고 population하므로 실패를 단일 metadata 대입의 rollback처럼 해석하지 않습니다. apply_vma_lock_flags()의 prev는 split·merge 뒤 새로 유효한 경계를 추적합니다. User buffer를 munlock했다고 외부 device의 pin까지 풀거나 모든 folio를 즉시 free하는 것도 아닙니다.
Permission 변경의 전체 helper
책에서는 상세 code를 생략했지만, flags 대입만으로 끝나지 않는다는 점을 아래 전체 함수에서 확인할 수 있습니다. do_mprotect_pkey()는 MAY 권한, architecture와 security hook을 검사하고 gather의 시작·종료를 짝지어 관리합니다. mprotect_fixup()은 commit accounting, merge·split, page protection 갱신과 failure 시 charge 반환을 연결합니다.
// SPDX-License-Identifier: GPL-2.0
/*
* mm/mprotect.c
*
* (C) Copyright 1994 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*
* Address space accounting code <alan@lxorguk.ukuu.org.uk>
* (C) Copyright 2002 Red Hat Inc, All Rights Reserved
*/
/* Upstream Linux v6.0: mm/mprotect.c:662-803
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int do_mprotect_pkey(unsigned long start, size_t len,
unsigned long prot, int pkey)
{
unsigned long nstart, end, tmp, reqprot;
struct vm_area_struct *vma, *prev;
int error;
const int grows = prot & (PROT_GROWSDOWN|PROT_GROWSUP);
const bool rier = (current->personality & READ_IMPLIES_EXEC) &&
(prot & PROT_READ);
struct mmu_gather tlb;
start = untagged_addr(start);
prot &= ~(PROT_GROWSDOWN|PROT_GROWSUP);
if (grows == (PROT_GROWSDOWN|PROT_GROWSUP)) /* can't be both */
return -EINVAL;
if (start & ~PAGE_MASK)
return -EINVAL;
if (!len)
return 0;
len = PAGE_ALIGN(len);
end = start + len;
if (end <= start)
return -ENOMEM;
if (!arch_validate_prot(prot, start))
return -EINVAL;
reqprot = prot;
if (mmap_write_lock_killable(current->mm))
return -EINTR;
/*
* If userspace did not allocate the pkey, do not let
* them use it here.
*/
error = -EINVAL;
if ((pkey != -1) && !mm_pkey_is_allocated(current->mm, pkey))
goto out;
vma = find_vma(current->mm, start);
error = -ENOMEM;
if (!vma)
goto out;
if (unlikely(grows & PROT_GROWSDOWN)) {
if (vma->vm_start >= end)
goto out;
start = vma->vm_start;
error = -EINVAL;
if (!(vma->vm_flags & VM_GROWSDOWN))
goto out;
} else {
if (vma->vm_start > start)
goto out;
if (unlikely(grows & PROT_GROWSUP)) {
end = vma->vm_end;
error = -EINVAL;
if (!(vma->vm_flags & VM_GROWSUP))
goto out;
}
}
if (start > vma->vm_start)
prev = vma;
else
prev = vma->vm_prev;
tlb_gather_mmu(&tlb, current->mm);
for (nstart = start ; ; ) {
unsigned long mask_off_old_flags;
unsigned long newflags;
int new_vma_pkey;
/* Here we know that vma->vm_start <= nstart < vma->vm_end. */
/* Does the application expect PROT_READ to imply PROT_EXEC */
if (rier && (vma->vm_flags & VM_MAYEXEC))
prot |= PROT_EXEC;
/*
* Each mprotect() call explicitly passes r/w/x permissions.
* If a permission is not passed to mprotect(), it must be
* cleared from the VMA.
*/
mask_off_old_flags = VM_READ | VM_WRITE | VM_EXEC |
VM_FLAGS_CLEAR;
new_vma_pkey = arch_override_mprotect_pkey(vma, prot, pkey);
newflags = calc_vm_prot_bits(prot, new_vma_pkey);
newflags |= (vma->vm_flags & ~mask_off_old_flags);
/* newflags >> 4 shift VM_MAY% in place of VM_% */
if ((newflags & ~(newflags >> 4)) & VM_ACCESS_FLAGS) {
error = -EACCES;
break;
}
/* Allow architectures to sanity-check the new flags */
if (!arch_validate_flags(newflags)) {
error = -EINVAL;
break;
}
error = security_file_mprotect(vma, reqprot, prot);
if (error)
break;
tmp = vma->vm_end;
if (tmp > end)
tmp = end;
if (vma->vm_ops && vma->vm_ops->mprotect) {
error = vma->vm_ops->mprotect(vma, nstart, tmp, newflags);
if (error)
break;
}
error = mprotect_fixup(&tlb, vma, &prev, nstart, tmp, newflags);
if (error)
break;
nstart = tmp;
if (nstart < prev->vm_end)
nstart = prev->vm_end;
if (nstart >= end)
break;
vma = prev->vm_next;
if (!vma || vma->vm_start != nstart) {
error = -ENOMEM;
break;
}
prot = reqprot;
}
tlb_finish_mmu(&tlb);
out:
mmap_write_unlock(current->mm);
return error;
}
// SPDX-License-Identifier: GPL-2.0
/*
* mm/mprotect.c
*
* (C) Copyright 1994 Linus Torvalds
* (C) Copyright 2002 Christoph Hellwig
*
* Address space accounting code <alan@lxorguk.ukuu.org.uk>
* (C) Copyright 2002 Red Hat Inc, All Rights Reserved
*/
/* Upstream Linux v6.0: mm/mprotect.c:538-657
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
int
mprotect_fixup(struct mmu_gather *tlb, struct vm_area_struct *vma,
struct vm_area_struct **pprev, unsigned long start,
unsigned long end, unsigned long newflags)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long oldflags = vma->vm_flags;
long nrpages = (end - start) >> PAGE_SHIFT;
unsigned long charged = 0;
bool try_change_writable;
pgoff_t pgoff;
int error;
if (newflags == oldflags) {
*pprev = vma;
return 0;
}
/*
* Do PROT_NONE PFN permission checks here when we can still
* bail out without undoing a lot of state. This is a rather
* uncommon case, so doesn't need to be very optimized.
*/
if (arch_has_pfn_modify_check() &&
(vma->vm_flags & (VM_PFNMAP|VM_MIXEDMAP)) &&
(newflags & VM_ACCESS_FLAGS) == 0) {
pgprot_t new_pgprot = vm_get_page_prot(newflags);
error = walk_page_range(current->mm, start, end,
&prot_none_walk_ops, &new_pgprot);
if (error)
return error;
}
/*
* If we make a private mapping writable we increase our commit;
* but (without finer accounting) cannot reduce our commit if we
* make it unwritable again. hugetlb mapping were accounted for
* even if read-only so there is no need to account for them here
*/
if (newflags & VM_WRITE) {
/* Check space limits when area turns into data. */
if (!may_expand_vm(mm, newflags, nrpages) &&
may_expand_vm(mm, oldflags, nrpages))
return -ENOMEM;
if (!(oldflags & (VM_ACCOUNT|VM_WRITE|VM_HUGETLB|
VM_SHARED|VM_NORESERVE))) {
charged = nrpages;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
newflags |= VM_ACCOUNT;
}
}
/*
* First try to merge with previous and/or next vma.
*/
pgoff = vma->vm_pgoff + ((start - vma->vm_start) >> PAGE_SHIFT);
*pprev = vma_merge(mm, *pprev, start, end, newflags,
vma->anon_vma, vma->vm_file, pgoff, vma_policy(vma),
vma->vm_userfaultfd_ctx, anon_vma_name(vma));
if (*pprev) {
vma = *pprev;
VM_WARN_ON((vma->vm_flags ^ newflags) & ~VM_SOFTDIRTY);
goto success;
}
*pprev = vma;
if (start != vma->vm_start) {
error = split_vma(mm, vma, start, 1);
if (error)
goto fail;
}
if (end != vma->vm_end) {
error = split_vma(mm, vma, end, 0);
if (error)
goto fail;
}
success:
/*
* vm_flags and vm_page_prot are protected by the mmap_lock
* held in write mode.
*/
vma->vm_flags = newflags;
/*
* We want to check manually if we can change individual PTEs writable
* if we can't do that automatically for all PTEs in a mapping. For
* private mappings, that's always the case when we have write
* permissions as we properly have to handle COW.
*/
if (vma->vm_flags & VM_SHARED)
try_change_writable = vma_wants_writenotify(vma, vma->vm_page_prot);
else
try_change_writable = !!(vma->vm_flags & VM_WRITE);
vma_set_page_prot(vma);
change_protection(tlb, vma, start, end, vma->vm_page_prot,
try_change_writable ? MM_CP_TRY_CHANGE_WRITABLE : 0);
/*
* Private VM_LOCKED VMA becoming writable: trigger COW to avoid major
* fault on access.
*/
if ((oldflags & (VM_WRITE | VM_SHARED | VM_LOCKED)) == VM_LOCKED &&
(newflags & VM_WRITE)) {
populate_vma_page_range(vma, start, end, NULL);
}
vm_stat_account(mm, oldflags, -nrpages);
vm_stat_account(mm, newflags, nrpages);
perf_event_mmap(vma);
return 0;
fail:
vm_unacct_memory(charged);
return error;
}
Read-only로 변경하는 요청은 단순히 앞으로의 fault 판단만 바꾸는 것이 아닙니다. 이미 존재하는 writable translation도 새 권한에 맞추어야 합니다. 반대로 writable로 바꾸더라도 CoW·write notification 때문에 각 PTE를 즉시 writable하게 만들 수 없는 경우가 있습니다.
DONTUNMAP과 FIXED의 실제 검사
v6.0의 vma_to_resize()는 DONTUNMAP에 대해 VM_DONTEXPAND·VM_PFNMAP을 거절하지만 private-anonymous-only 검사로 제한하지 않습니다. 따라서 그 제한을 이 tag에도 그대로 적용하면 맞지 않습니다. DONTUNMAP의 이동·동일 길이 조건과 source refault를 구분해야 하며, 즉시 모든 source PTE에 zero page를 설치한다고 설명하지 않겠습니다.
// 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
*/
/* Upstream Linux v6.0: mm/mremap.c:725-777
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static struct vm_area_struct *vma_to_resize(unsigned long addr,
unsigned long old_len, unsigned long new_len, unsigned long flags)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long pgoff;
vma = vma_lookup(mm, addr);
if (!vma)
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->vm_flags & (VM_DONTEXPAND | VM_PFNMAP)))
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 (mlock_future_check(mm, vma->vm_flags, new_len - old_len))
return ERR_PTR(-EAGAIN);
if (!may_expand_vm(mm, vma->vm_flags,
(new_len - old_len) >> PAGE_SHIFT))
return ERR_PTR(-ENOMEM);
return vma;
}
// 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
*/
/* Upstream Linux v6.0: mm/mremap.c:779-864
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
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 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 unmapping 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)
goto out;
old_len = new_len;
}
vma = vma_to_resize(addr, old_len, new_len, flags);
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 out;
/* 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);
out:
return ret;
}
mremap_to()에는 FIXED의 destination을 do_munmap()하는 branch가 실제로 있습니다. 주소가 비어 있을 것이라는 추측으로 임의 목표 주소를 지정하면 기존 mapping을 파괴할 수 있습니다. 이 글은 그 branch를 실행하는 실습을 제공하지 않습니다. 기존 pointer의 offset을 새 반환 base에 다시 적용해야 하는 문제도 object lifetime을 pin하는 문제와 다릅니다.
Source: v6.0 mm/mlock.c, mm/mprotect.c, mm/mremap.c. 추가 helper는 책 listing 복원이 아니라 공식 source의 독립적인 보충입니다. 원래 notice와 COPYING, GPL-2.0를 보존했습니다. v6.0은 historical reference이고 book exact tag는 미확정이며 v6.18은 별도 고정 비교 tag입니다. Kernel compile·runtime 검증은 하지 않았습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.2.1–§8.2.3. mlock(2), mprotect(2), mremap(2), Linux v6.18 mm/mremap.c를 참고하실 수 있습니다.