mmap이 VMA를 연결하기 전에 확인하는 것들

file mapping을 만들 때 kernel은 단순히 새 vm_area_struct 하나를 allocation하지 않습니다. 주소와 길이를 검증하고, file의 권한과 operation을 확인하며, commit과 locked-memory를 청구합니다. 이어 filesystem callback이 VMA를 바꿀 수도 있습니다. 실패 경로를 함께 읽어야 이 과정에서 누가 file reference와 accounting을 소유하는지 드러납니다.

syscall 인자에서 mapping 정책까지

x86-64 syscall entry는 byte offset의 page 정렬을 검사하고 page offset으로 변환합니다. ksys_mmap_pgoff()는 file-backed 요청에 fget()으로 file을 확보하고 아래 계층에 넘깁니다. vm_mmap_pgoff()는 security hook, address-space locking, 실제 mapping, 필요한 population을 조정하는 경계입니다.

byte offset의 하위 PAGE_SHIFT bits가 남으면 x86-64 entry는 EINVAL로 거절합니다. page offset을 받는 architecture의 mmap2 interface와 같은 단위라고 가정하면 안 됩니다. ksys_mmap_pgoff()는 file-backed 요청의 fd를 audit hook에 전달하고 fget()으로 file을 얻습니다. 이 reference는 syscall 동안 fd가 닫혀도 file을 사용할 수 있게 하는 임시 소유권입니다. 잘못된 fd는 EBADF로 끝나고, file을 얻었으면 mapping 시도 뒤 fput()합니다. 최종 VMA가 갖는 reference는 이 임시 reference와 별도로 취득합니다. hugetlb file 또는 anonymous hugetlb 요청은 page-size 정렬과 별도 backing 준비도 필요합니다.

vm_mmap_pgoff()는 security_mmap_file()을 먼저 통과한 뒤 killable write lock을 얻습니다. lock 대기가 중단되면 EINTR이며 mapping을 실행하지 않습니다. population 길이는 처음에 0으로 두고, 성공한 mapping에 VM_LOCKED가 적용되거나 MAP_POPULATE가 있고 MAP_NONBLOCK이 없을 때 설정합니다. lock을 놓고 userfaultfd 완료 처리를 한 다음 population을 수행합니다. 이 순서 덕분에 주소 index 수정의 critical section과 실제 fault 작업이 구별됩니다.

RB-tree 기반 구현의 do_mmap()은 length 0, 정렬 뒤 overflow, pgoff + page_count overflow, mapping 개수 초과를 먼저 걸러 냅니다. hint는 최저 허용 주소와 page 정렬을 고려합니다. MAP_FIXED_NOREPLACE의 실제 교차 검사와 주소 선택도 mapping 전에 필요합니다. 요청 권한에서 도출한 VMA flags와 mm->def_flags, MAYREAD·MAYWRITE·MAYEXEC 같은 최대 가능 권한은 현재 접근 권한과 구분하여 설정합니다.

여기서는 실패 값도 구분할 만합니다. length 0은 EINVAL, PAGE_ALIGN() 뒤 0이 되는 wrap과 VMA 개수 한계는 ENOMEM, page offset에 page 수를 더한 overflow는 EOVERFLOW입니다. non-FIXED hint는 page 경계를 내림하고, 0이 아닌 hint가 mmap_min_addr보다 낮으면 최소 주소 쪽으로 올립니다. NULL hint는 주소를 고르라는 뜻을 보존합니다. MAP_FIXED_NOREPLACE는 주소 선택 단계에 FIXED 성격을 전달하되, 실제 교차하는 VMA가 발견되면 EEXIST로 거절합니다. 기존 VMA를 덮어쓴 뒤 검사하는 순서가 아닙니다.

READ_IMPLIES_EXEC personality에서는 READ 요청에 EXEC를 추가할 수 있지만 noexec file mount에서는 그렇게 확대하지 않습니다. calc_vm_prot_bits()는 protection 요청을, calc_vm_flag_bits()는 mapping flags를 VMA bits로 옮깁니다. 여기에 default flags와 MAY bits를 합쳐도 즉시 writable·executable PTE가 생기지는 않습니다. MAY bits는 이후 protection 변경을 허용할 상한이지 현재 hardware 권한이 아닙니다.

MAP_LOCKED에는 can_do_mlock()의 기본 허용 검사와 실제 증가량이 memlock limit을 넘는지의 검사가 따로 있습니다. 호출 thread에 capability가 없고 limit도 0인 경우와, 기본 검사를 통과했지만 이번 길이가 너무 큰 경우를 구별하는 구조입니다.

전자는 EPERM, future-lock 증가량이 한계를 넘는 후자는 EAGAIN 경로입니다. CAP_IPC_LOCK에 따른 면제 여부와 이미 잠근 locked_vm을 함께 보므로 단순히 이번 length만 limit과 비교해서도 안 됩니다. helper 이름은 구현 계열에 따라 mlock_future_check() 또는 mlock_future_ok() 등으로 다르지만 권한과 양의 검사는 별개입니다.

Linux capability는 process 전체에 하나씩 붙는 권한 bit가 아니라 thread별 속성입니다. 따라서 이 면제는 호출 thread의 CAP_IPC_LOCK을 기준으로 읽어야 합니다. 주소 공간과 그 locked_vm accounting을 공유한다는 사실과 capability의 적용 단위는 서로 다릅니다. capabilities(7)

file mapping의 권한 검사는 두 번 다른 질문을 합니다

file_mmap_ok()는 현재 EOF만 보는 함수가 아닙니다. 허용 가능한 파일 크기와 offset 계산을 확인하여 특히 좁은 integer index에서 overflow가 생기지 않게 합니다. 그다음에는 mapping type과 file의 open mode를 조합합니다. shared writable mapping에는 write-open이 필요하며 append-only file을 임의 위치 writable mapping으로 우회할 수 없습니다. swap file도 일반 shared-write 대상으로 취급하지 않습니다.

shared validation을 마친 뒤 private case와 공통으로 read-open, noexec mount, filesystem의 mmap operation 지원을 검사합니다. noexec에서는 EXEC 요청을 거부하거나 미래 실행 허용 상태도 제한합니다. file-backed grow-down stack처럼 의미가 맞지 않는 조합도 거절합니다. filesystem이 지원하는 추가 flags와 legacy 허용 mask는 구별해야 합니다.

세부 조건을 놓치면 read-only mapping에서도 결과를 잘못 예상하기 쉽습니다. RB-tree 기반 분기에서 append-only 검사는 IS_APPEND(inode)와 file의 FMODE_WRITE 조합을 봅니다. 현재 요청의 PROT_WRITE만 보는 검사가 아닙니다. write-open descriptor의 shared read-only 요청도 이 조건에 걸릴 수 있습니다. shared mapping을 read-only descriptor로 만들 때에는 VM_MAYSHARE는 남기지만 VM_SHARED와 VM_MAYWRITE를 지우는 경로가 있습니다. 사용자 API의 SHARED 선택과 내부의 write-sharing 가능성을 한 bit로 동일시할 수 없습니다.

MAP_TYPE mask는 mapping 종류를 분리합니다. legacy SHARED는 알려진 legacy flags로 제한한 뒤 VALIDATE의 공통 검사로 내려가고, VALIDATE는 지원 mask 밖의 요청을 EOPNOTSUPP로 거절합니다. 이어 private와 공통으로 read-open을 확인하며, noexec mount에서는 EXEC 요청을 거절하고 VM_MAYEXEC도 제거합니다. file 크기 검사는 일반 file의 최대 크기와 unsigned offset 계산의 예외를 처리하는 것이므로 현재 EOF를 넘는 offset을 전부 거절하는 검사로 읽으면 안 됩니다. 나중의 fault가 backing을 제공할 수 있는지는 다른 문제입니다.

filesystem interface의 이름에도 version 차이가 있습니다. RB-tree 계열의 mmap_supported_flags와 단순 f_op->mmap 확인을 v6.18 그대로의 구조체 설명으로 옮겨서는 안 됩니다. v6.18은 FOP_MMAP_SYNC를 포함한 지원 판단과 can_mmap_file()을 사용합니다. 유지되는 의미는 filesystem이 알지 못하는 계약을 VALIDATE로 몰래 수락하지 않는다는 점입니다.

오류도 이 구분을 반영합니다. file size·offset 상한 위반은 EOVERFLOW, 필요한 read·write open mode의 부재는 EACCES, swap file의 shared-write 요청은 ETXTBSY입니다. noexec mount에서의 EXEC 요청은 EPERM, mmap operation을 제공하지 않는 file은 ENODEV, 유효하지 않은 mapping type이나 file-backed growth flags는 EINVAL입니다. file_mmap_ok()는 64-bit maxsize에서 길이를 먼저 검사·차감한 다음 page 단위 offset과 비교해, 단순한 끝 주소 덧셈의 overflow를 피합니다. 이 구현에서 0인 최대 크기는 특수한 무상한 sentinel이므로 크기 0의 file이라는 뜻이 아닙니다.

anonymous private 요청의 vm_pgoff는 처음에는 virtual page offset에 대응하도록 정합니다. 이 값은 rmap의 논리 좌표가 되므로 나중에 mremap으로 주소를 옮겨도 함부로 새 VA로 바꾸지 않습니다. anonymous shared는 shmem backing을 만들며 offset은 그 내부 객체 기준으로 정리합니다.

이 RB-tree 계열의 anonymous switch는 SHARED와 PRIVATE를 받습니다. SHARED에서는 offset을 0으로 만들고 shared flags를 세우며 grow-down·grow-up 조합을 거절합니다. PRIVATE에서는 addr >> PAGE_SHIFT를 초기 offset으로 사용합니다. 이 분기의 MAP_SHARED_VALIDATE는 file-backed case와 달리 유효한 anonymous 종류가 아닙니다. 다만 "anonymous 종류는 언제나 둘뿐"이라는 version 독립 규칙은 아닙니다. v6.18에는 MAP_DROPPABLE 분기도 추가되어 있습니다. application에서 anonymous로 요청했어도 shared 경로의 완성된 VMA는 shmem file을 가집니다.

새 VMA가 필요하지 않을 수도 있습니다

mmap_region()의 첫 과제는 virtual-space 한계와 겹침을 조정하는 것입니다. FIXED replacement에서는 겹치는 mapping을 제거한 뒤의 증가량으로 limit을 판단해야 하는 경우가 있습니다. 겹침 제거가 가능한 요청이라는 사실과 새 mapping이 끝까지 성공한다는 사실은 다르므로 FIXED를 rollback 가능한 transaction처럼 생각해서는 안 됩니다.

commit을 청구하고 기존 이웃과 merge할 수 있으면 새 VMA allocation을 피합니다. 그렇지 않으면 경계, flags, file offset과 protection을 초기화합니다. file mapping은 file reference와 writable-mapping 추적을 얻고 filesystem의 mmap callback을 호출합니다. callback이 flags나 backing을 바꾸었으면 최초에는 불가능했던 merge가 가능해질 수 있어 다시 확인합니다.

처음의 may_expand_vm()이 실패하더라도 FIXED replacement라면 기존 겹침의 page 수를 빼 순증가량으로 다시 검사할 수 있습니다. RB-tree 경로의 munmap_vma_range()는 겹침을 지우는 동시에 연결에 필요한 prev·rb_link·rb_parent를 준비합니다. 그 뒤 accountable_mapping()에 해당할 때만 security_vm_enough_memory_mm()으로 청구하고 VM_ACCOUNT를 더합니다. 새 VMA allocation에 실패하면 이미 청구한 page 수만 반납합니다. 주소 범위의 변화, commit 청구, VMA object의 allocation은 이처럼 서로 다른 자원입니다.

callback 뒤에는 소유권을 다시 확인합니다

file callback을 부르기 전 get_file()로 VMA의 reference를 확보합니다. shared 경로의 mapping_map_writable()은 file reference가 아니라 address_space의 별도 writable-mapping 추적이며, 이 준비 단계의 증가분은 성공·실패 경로에서 짝을 맞추어 내립니다. 둘을 하나의 refcount로 설명하면 어떤 자원을 반납해야 하는지 놓치게 됩니다.

callback은 vm_flags뿐 아니라 vm_file을 교체할 수도 있습니다. flags 변경 뒤 두 번째 merge가 성공하면 더 이상 필요 없는 임시 VMA를 버려야 하는데, 이때 fput()의 대상은 현재의 vma->vm_file입니다. callback이 이미 원 file의 reference를 내리고 새 file을 넣었다면 syscall 입력의 file을 다시 fput()하는 것은 이중 해제가 됩니다. merge된 VMA가 살아남는 것과 임시 VMA의 reference를 정리하는 일은 별개입니다.

일부 device callback이 vm_start를 바꾸는 경우를 경고하는 WARN_ON_ONCE()도 있습니다. 이것은 callback이 아무 주소로 옮겨도 안전하다는 허가가 아닙니다. 바뀐 시작 주소를 사용하면서 이전 주소로 계산한 prev·RB insertion hints를 그대로 신뢰하면 안 된다는 구조적 제약입니다. 마지막에는 arch_validate_flags()를 통과한 VMA만 index에 연결합니다.

private anonymous VMA에는 anonymous operation 상태를 설정하고, shared anonymous에는 shmem_zero_setup()이 unlinked shmem file과 operation을 연결합니다. 이 객체는 이름이 보이지 않아도 reference로 살아 있습니다. descriptor를 닫는 것, pathname을 unlink하는 것, VMA를 제거하는 것은 다른 수명 사건입니다.

private 경로의 vma_set_anonymous()는 allocation 때의 dummy operation 상태를 anonymous 판정에 맞게 정리합니다. 반면 shmem_zero_setup()은 VMA 길이의 내부 file을 만들고 기존 vm_file이 있으면 그 reference를 내린 뒤 shmem_vm_ops를 연결합니다. shmem은 일반적인 ramfs와 같은 backing이라고 단순화할 수 없습니다. 보통의 shmem에는 swap을 지원하는 자체 동작이 있습니다.

이전 shmem_zero_setup()의 S_PRIVATE 인자에는 XFS·SELinux의 lock dependency 문제를 피하려는 내부 객체 생성 문맥이 담겨 있습니다. 이를 사용자에게 보이는 file의 security 검사를 전부 우회한다는 규칙으로 일반화해서는 안 됩니다. 내부 unlinked inode의 생성 검사를 조정해도 VMA에 관한 검사는 남습니다. 이 조건과 reference 교체는 Linux v6.0의 shmem_zero_setup()에서 확인할 수 있습니다.

writable VMA에 read-only PTE가 필요한 이유

VMA를 주소 index에 연결한 뒤에도 statistics, locked accounting, soft-dirty 설정과 page protection 계산이 남습니다. 특수 mapping·hugetlb·DAX·gate 등은 일반 mlock 처리에서 별도 제약을 받습니다. 오류로 빠질 때에는 이미 얻은 file reference, writable mapping 추적, 새 VMA와 commit 청구 중 실제로 획득한 것만 되돌려야 합니다.

device driver가 PTE 일부를 설치한 뒤 callback이 실패할 수도 있습니다. file reference와 임시 VMA만 해제하고 끝내면 그 translation이 남으므로 unmap_region()으로 부분 mapping까지 정리합니다. 실패 label은 항상 같은 초기 상태에서 시작하지 않습니다. writable 추적 증가 전의 실패, file reference 취득 뒤의 실패, commit 청구 뒤의 실패가 각각 반납할 자원을 달리하는 이유입니다.

성공 경로는 vm_stat_account()와 필요한 locked_vm 증가를 수행합니다. 일반 mlock을 적용할 수 없는 special·hugetlb·DAX·gate mapping에서는 VM_LOCKED를 지우고 같은 locked 통계를 올리지 않습니다. 새로 만들거나 늘린 범위에는 VM_SOFTDIRTY를 세웁니다. 같은 VA를 unmap한 뒤 전혀 다른 data를 다시 mapping했을 때, 이전의 깨끗한 범위와 구별할 수 있어야 하기 때문입니다.

vma_wants_writenotify()는 shared writable mapping에도 첫 write를 trap할 필요가 있는지 판단합니다. filesystem의 page_mkwrite·pfn_mkwrite, dirty tracking과 soft-dirty가 그 이유입니다. VMA의 VM_WRITE를 지우는 것이 아니라 초기 PTE protection을 제한하여 filesystem이 변경을 준비하고 dirty 상태를 추적할 기회를 확보합니다. raw PFN mapping과 pgprot을 보존할 수 없는 경우는 같은 방법을 무조건 적용할 수 없습니다.

Linux v6.0의 RB-tree 계열 predicate는 조건의 순서도 중요합니다. 먼저 WRITE와 SHARED가 모두 있어야 하며, page_mkwrite 또는 pfn_mkwrite가 있으면 notification을 선택합니다. 그다음 기존 vm_page_prot를 일반 수정 연산으로 보존할 수 없는 경우를 제외합니다. soft-dirty 추적이 필요하고 hugetlb가 아니면 notification을 택합니다. 여기서 soft-dirty 추적 필요 판정은 VM_SOFTDIRTY가 아직 없다는 상태를 포함하므로 이름만 보고 반대로 읽기 쉽습니다. 이후 VM_PFNMAP을 제외하고, 마지막으로 file의 address_space가 writeback을 지원하는지 봅니다. 따라서 PFN mapping이면 앞의 callback 검사와 무관하게 언제나 false라는 설명도 틀립니다. 이 조건은 v6.0 전체 함수에서 확인할 수 있으며, 뒤에 수록한 v5.18과 soft-dirty 분기를 구분해야 합니다.

vma_set_page_prot()는 local vm_flags 사본에서 VM_SHARED를 제외해 write-notify에 맞는 protection을 계산할 수 있습니다. VMA 본체를 PRIVATE로 바꾸는 것은 아닙니다. 계산한 vm_page_prot의 저장에 WRITE_ONCE()를 쓰는 이유는 remove_protection_ptes()처럼 mmap_lock 없이 이 값을 읽는 경로가 있기 때문입니다. 이 한 번의 저장은 VMA 수명을 보호하는 lock이나 mapping 변경 전체의 동기화를 대체하지 않습니다.

이 설명의 linked-list/RB-tree 연결과 __vma_adjust() 중심 helper는 RB-tree 기반 구현의 구현 계열입니다. v6.18의 mapping 및 VMA 작업은 Maple Tree와 별도 helper로 재구성되어 있습니다. 동일한 함수 이름 몇 개가 남았다는 이유로 Listing 전체가 현재 실행 경로와 같다고 볼 수는 없습니다.

v5.18 syscall entry에서 population까지

이제 file reference와 lock 경계를 전체 함수로 이어 보겠습니다. 아래 코드는 모두 고정 tag v5.18입니다. 기존의 v6.0 조건 설명이나 v6.18의 Maple Tree 경로와 같은 버전으로 합치지 않습니다. 긴 함수를 여러 조각의 가짜 함수로 나누지 않고 실제 정의 단위로 제공합니다. 전체 source 파일은 v5.18 코드 묶음에서 받으실 수 있습니다. 각 파일의 원 header와 license를 보존했으며, 한국어 해설과 upstream code의 license는 구분합니다. 이 묶음은 kernel 전체 build tree나 userspace 실행 예제가 아닙니다.

x86-64 mmap entry

공식 v5.18 arch/x86/kernel/sys_x86_64.c, L89–L97 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0
/* Linux v5.18: arch/x86/kernel/sys_x86_64.c */
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, off)
{
    if (off & ~PAGE_MASK)
        return -EINVAL;

    return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

ksys_mmap_pgoff()

공식 v5.18 mm/mmap.c, L1590–L1633 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
                  unsigned long prot, unsigned long flags,
                  unsigned long fd, unsigned long pgoff)
{
    struct file *file = NULL;
    unsigned long retval;

    if (!(flags & MAP_ANONYMOUS)) {
        audit_mmap_fd(fd, flags);
        file = fget(fd);
        if (!file)
            return -EBADF;
        if (is_file_hugepages(file)) {
            len = ALIGN(len, huge_page_size(hstate_file(file)));
        } else if (unlikely(flags & MAP_HUGETLB)) {
            retval = -EINVAL;
            goto out_fput;
        }
    } else if (flags & MAP_HUGETLB) {
        struct hstate *hs;

        hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
        if (!hs)
            return -EINVAL;

        len = ALIGN(len, huge_page_size(hs));
        /*
         * VM_NORESERVE is used because the reservations will be
         * taken when vm_ops->mmap() is called
         */
        file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
                VM_NORESERVE,
                HUGETLB_ANONHUGE_INODE,
                (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
        if (IS_ERR(file))
            return PTR_ERR(file);
    }

    retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
    if (file)
        fput(file);
    return retval;
}

hugetlb file 요청과 anonymous hugetlb 요청이 별개이며, anonymous hugetlb에서도 실제 file 객체를 생성해 아래 계층에 넘깁니다. 함수 끝의 fput()은 이 진입점이 확보한 reference를 반납합니다. 성공한 VMA의 reference까지 함께 없애는 처리는 아닙니다.

vm_mmap_pgoff()

공식 v5.18 mm/util.c, L506–L527 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/* Linux v5.18: mm/util.c */
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long pgoff)
{
    unsigned long ret;
    struct mm_struct *mm = current->mm;
    unsigned long populate;
    LIST_HEAD(uf);

    ret = security_mmap_file(file, prot, flag);
    if (!ret) {
        if (mmap_write_lock_killable(mm))
            return -EINTR;
        ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
                  &uf);
        mmap_write_unlock(mm);
        userfaultfd_unmap_complete(mm, &uf);
        if (populate)
            mm_populate(ret, populate);
    }
    return ret;
}

security hook이 먼저 실행되고 그 성공 뒤에만 write lock을 얻습니다. populate는 do_mmap()이 초기화하며, mapping 실패에서는 population을 요청하지 않습니다. population의 반환 상태를 별도 mmap error로 바꾸는 코드가 없다는 점도 best-effort 성격과 연결됩니다.

v5.18 do_mmap()과 입력 경계 검사

round_hint_to_min()

공식 v5.18 mm/mmap.c, L1350–L1357 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
static inline unsigned long round_hint_to_min(unsigned long hint)
{
    hint &= PAGE_MASK;
    if (((void *)hint != NULL) &&
        (hint < mmap_min_addr))
        return PAGE_ALIGN(mmap_min_addr);
    return hint;
}

file_mmap_ok()

공식 v5.18 mm/mmap.c, L1395–L1406 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
static inline bool file_mmap_ok(struct file *file, struct inode *inode,
                unsigned long pgoff, unsigned long len)
{
    u64 maxsize = file_mmap_size_max(file, inode);

    if (maxsize && len > maxsize)
        return false;
    maxsize -= len;
    if (pgoff > maxsize >> PAGE_SHIFT)
        return false;
    return true;
}

file_mmap_size_max()의 정의도 같은 전체 source 파일에 있습니다. 이 함수가 돌려준 0은 무상한 sentinel이며, maxsize -= len의 unsigned wrap을 포함한 계산으로 offset을 검사합니다. 파일의 현재 i_size가 0이라는 뜻이 아닙니다.

do_mmap()

공식 v5.18 mm/mmap.c, L1411–L1588 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
unsigned long do_mmap(struct file *file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, unsigned long pgoff,
            unsigned long *populate, struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    vm_flags_t vm_flags;
    int pkey = 0;

    *populate = 0;

    if (!len)
        return -EINVAL;

    /*
     * Does the application expect PROT_READ to imply PROT_EXEC?
     *
     * (the exception is when the underlying filesystem is noexec
     *  mounted, in which case we dont add PROT_EXEC.)
     */
    if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
        if (!(file && path_noexec(&file->f_path)))
            prot |= PROT_EXEC;

    /* force arch specific MAP_FIXED handling in get_unmapped_area */
    if (flags & MAP_FIXED_NOREPLACE)
        flags |= MAP_FIXED;

    if (!(flags & MAP_FIXED))
        addr = round_hint_to_min(addr);

    /* Careful about overflows.. */
    len = PAGE_ALIGN(len);
    if (!len)
        return -ENOMEM;

    /* offset overflow? */
    if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
        return -EOVERFLOW;

    /* Too many mappings? */
    if (mm->map_count > sysctl_max_map_count)
        return -ENOMEM;

    /* Obtain the address to map to. we verify (or select) it and ensure
     * that it represents a valid section of the address space.
     */
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    if (flags & MAP_FIXED_NOREPLACE) {
        if (find_vma_intersection(mm, addr, addr + len))
            return -EEXIST;
    }

    if (prot == PROT_EXEC) {
        pkey = execute_only_pkey(mm);
        if (pkey < 0)
            pkey = 0;
    }

    /* Do simple checking here so the lower-level routines won't have
     * to. we assume access permissions have been handled by the open
     * of the memory object, so we don't do any here.
     */
    vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
            mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

    if (flags & MAP_LOCKED)
        if (!can_do_mlock())
            return -EPERM;

    if (mlock_future_check(mm, vm_flags, len))
        return -EAGAIN;

    if (file) {
        struct inode *inode = file_inode(file);
        unsigned long flags_mask;

        if (!file_mmap_ok(file, inode, pgoff, len))
            return -EOVERFLOW;

        flags_mask = LEGACY_MAP_MASK | file->f_op->mmap_supported_flags;

        switch (flags & MAP_TYPE) {
        case MAP_SHARED:
            /*
             * Force use of MAP_SHARED_VALIDATE with non-legacy
             * flags. E.g. MAP_SYNC is dangerous to use with
             * MAP_SHARED as you don't know which consistency model
             * you will get. We silently ignore unsupported flags
             * with MAP_SHARED to preserve backward compatibility.
             */
            flags &= LEGACY_MAP_MASK;
            fallthrough;
        case MAP_SHARED_VALIDATE:
            if (flags & ~flags_mask)
                return -EOPNOTSUPP;
            if (prot & PROT_WRITE) {
                if (!(file->f_mode & FMODE_WRITE))
                    return -EACCES;
                if (IS_SWAPFILE(file->f_mapping->host))
                    return -ETXTBSY;
            }

            /*
             * Make sure we don't allow writing to an append-only
             * file..
             */
            if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
                return -EACCES;

            vm_flags |= VM_SHARED | VM_MAYSHARE;
            if (!(file->f_mode & FMODE_WRITE))
                vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
            fallthrough;
        case MAP_PRIVATE:
            if (!(file->f_mode & FMODE_READ))
                return -EACCES;
            if (path_noexec(&file->f_path)) {
                if (vm_flags & VM_EXEC)
                    return -EPERM;
                vm_flags &= ~VM_MAYEXEC;
            }

            if (!file->f_op->mmap)
                return -ENODEV;
            if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                return -EINVAL;
            break;

        default:
            return -EINVAL;
        }
    } else {
        switch (flags & MAP_TYPE) {
        case MAP_SHARED:
            if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
                return -EINVAL;
            /*
             * Ignore pgoff.
             */
            pgoff = 0;
            vm_flags |= VM_SHARED | VM_MAYSHARE;
            break;
        case MAP_PRIVATE:
            /*
             * Set pgoff according to addr for anon_vma.
             */
            pgoff = addr >> PAGE_SHIFT;
            break;
        default:
            return -EINVAL;
        }
    }

    /*
     * Set 'VM_NORESERVE' if we should not account for the
     * memory use of this mapping.
     */
    if (flags & MAP_NORESERVE) {
        /* We honor MAP_NORESERVE if allowed to overcommit */
        if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
            vm_flags |= VM_NORESERVE;

        /* hugetlb applies strict overcommit unless MAP_NORESERVE */
        if (file && is_file_hugepages(file))
            vm_flags |= VM_NORESERVE;
    }

    addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
    if (!IS_ERR_VALUE(addr) &&
        ((vm_flags & VM_LOCKED) ||
         (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
        *populate = len;
    return addr;
}

실패할 수 있는 단계가 주소 선택 전과 뒤에 모두 있습니다. MAP_FIXED_NOREPLACE가 FIXED 의미를 전달한 뒤 실제 교차를 EEXIST로 거절하는 순서, read-open과 write-open의 별도 조건, private anonymous의 초기 pgoff 설정이 한 함수 안에서 연결됩니다. 마지막 populate 설정도 mmap_region()의 성공 여부를 먼저 확인합니다.

v5.18 mmap_region()의 전체 자원 수명

공식 v5.18 mm/mmap.c, L1720–L1894 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
unsigned long mmap_region(struct file *file, unsigned long addr,
        unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
        struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev, *merge;
    int error;
    struct rb_node **rb_link, *rb_parent;
    unsigned long charged = 0;

    /* Check against address space limit. */
    if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
        unsigned long nr_pages;

        /*
         * MAP_FIXED may remove pages of mappings that intersects with
         * requested mapping. Account for the pages it would unmap.
         */
        nr_pages = count_vma_pages_range(mm, addr, addr + len);

        if (!may_expand_vm(mm, vm_flags,
                    (len >> PAGE_SHIFT) - nr_pages))
            return -ENOMEM;
    }

    /* Clear old maps, set up prev, rb_link, rb_parent, and uf */
    if (munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf))
        return -ENOMEM;
    /*
     * Private writable mapping: check memory availability
     */
    if (accountable_mapping(file, vm_flags)) {
        charged = len >> PAGE_SHIFT;
        if (security_vm_enough_memory_mm(mm, charged))
            return -ENOMEM;
        vm_flags |= VM_ACCOUNT;
    }

    /*
     * Can we just expand an old mapping?
     */
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
    if (vma)
        goto out;

    /*
     * Determine the object being mapped and call the appropriate
     * specific mapper. the address has already been validated, but
     * not unmapped, but the maps are removed from the list.
     */
    vma = vm_area_alloc(mm);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }

    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = vm_get_page_prot(vm_flags);
    vma->vm_pgoff = pgoff;

    if (file) {
        if (vm_flags & VM_SHARED) {
            error = mapping_map_writable(file->f_mapping);
            if (error)
                goto free_vma;
        }

        vma->vm_file = get_file(file);
        error = call_mmap(file, vma);
        if (error)
            goto unmap_and_free_vma;

        /* Can addr have changed??
         *
         * Answer: Yes, several device drivers can do it in their
         *         f_op->mmap method. -DaveM
         * Bug: If addr is changed, prev, rb_link, rb_parent should
         *      be updated for vma_link()
         */
        WARN_ON_ONCE(addr != vma->vm_start);

        addr = vma->vm_start;

        /* If vm_flags changed after call_mmap(), we should try merge vma again
         * as we may succeed this time.
         */
        if (unlikely(vm_flags != vma->vm_flags && prev)) {
            merge = vma_merge(mm, prev, vma->vm_start, vma->vm_end, vma->vm_flags,
                NULL, vma->vm_file, vma->vm_pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
            if (merge) {
                /* ->mmap() can change vma->vm_file and fput the original file. So
                 * fput the vma->vm_file here or we would add an extra fput for file
                 * and cause general protection fault ultimately.
                 */
                fput(vma->vm_file);
                vm_area_free(vma);
                vma = merge;
                /* Update vm_flags to pick up the change. */
                vm_flags = vma->vm_flags;
                goto unmap_writable;
            }
        }

        vm_flags = vma->vm_flags;
    } else if (vm_flags & VM_SHARED) {
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    } else {
        vma_set_anonymous(vma);
    }

    /* Allow architectures to sanity-check the vm_flags */
    if (!arch_validate_flags(vma->vm_flags)) {
        error = -EINVAL;
        if (file)
            goto unmap_and_free_vma;
        else
            goto free_vma;
    }

    vma_link(mm, vma, prev, rb_link, rb_parent);
    /* Once vma denies write, undo our temporary denial count */
unmap_writable:
    if (file && vm_flags & VM_SHARED)
        mapping_unmap_writable(file->f_mapping);
    file = vma->vm_file;
out:
    perf_event_mmap(vma);

    vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
    if (vm_flags & VM_LOCKED) {
        if ((vm_flags & VM_SPECIAL) || vma_is_dax(vma) ||
                    is_vm_hugetlb_page(vma) ||
                    vma == get_gate_vma(current->mm))
            vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
        else
            mm->locked_vm += (len >> PAGE_SHIFT);
    }

    if (file)
        uprobe_mmap(vma);

    /*
     * New (or expanded) vma always get soft dirty status.
     * Otherwise user-space soft-dirty page tracker won't
     * be able to distinguish situation when vma area unmapped,
     * then new mapped in-place (which must be aimed as
     * a completely new data area).
     */
    vma->vm_flags |= VM_SOFTDIRTY;

    vma_set_page_prot(vma);

    return addr;

unmap_and_free_vma:
    fput(vma->vm_file);
    vma->vm_file = NULL;

    /* Undo any partial mapping done by a device driver. */
    unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
    charged = 0;
    if (vm_flags & VM_SHARED)
        mapping_unmap_writable(file->f_mapping);
free_vma:
    vm_area_free(vma);
unacct_error:
    if (charged)
        vm_unacct_memory(charged);
    return error;
}

unmap_and_free_vma는 filesystem callback이 부분 설치한 translation까지 제거하는 경로입니다. 다만 여기 수록한 v5.18의 charged = 0은 올바른 commit 반납의 예로 따라서는 안 됩니다. 이 tag의 unmap_region()은 page table과 TLB를 정리하지만 commit을 반납하지 않으므로, 이 대입은 뒤의 vm_unacct_memory(charged)까지 건너뛰게 합니다. upstream은 commit 7f82f922319e에서 이 대입을 제거하여 accounting 불균형을 고쳤습니다. 고정 tag의 전체 code를 보여 주는 것과 그 code의 모든 동작이 올바르다고 보증하는 것은 다릅니다. 반면 VMA allocation 실패는 unacct_error에서 아직 남은 청구를 직접 반납합니다. 정상 unmap의 remove_vma_list()가 수행하는 commit 반납과도 구별해야 합니다. 실패 label마다 확보한 자원이 같지 않으므로, label 이름만 보고 한 종류의 cleanup으로 치환하면 안 됩니다.

shmem_zero_setup()

공식 v5.18 mm/shmem.c, L4128–L4155 · 전체 source 파일

/*
 * Resizable virtual memory filesystem for Linux.
 *
 * Copyright (C) 2000 Linus Torvalds.
 *       2000 Transmeta Corp.
 *       2000-2001 Christoph Rohland
 *       2000-2001 SAP AG
 *       2002 Red Hat Inc.
 * Copyright (C) 2002-2011 Hugh Dickins.
 * Copyright (C) 2011 Google Inc.
 * Copyright (C) 2002-2005 VERITAS Software Corporation.
 * Copyright (C) 2004 Andi Kleen, SuSE Labs
 *
 * Extended attribute support for tmpfs:
 * Copyright (c) 2004, Luke Kenneth Casson Leighton <lkcl@lkcl.net>
 * Copyright (c) 2004 Red Hat, Inc., James Morris <jmorris@redhat.com>
 *
 * tiny-shmem:
 * Copyright (c) 2004, 2008 Matt Mackall <mpm@selenic.com>
 *
 * This file is released under the GPL.
 */
/* Linux v5.18: mm/shmem.c */
int shmem_zero_setup(struct vm_area_struct *vma)
{
    struct file *file;
    loff_t size = vma->vm_end - vma->vm_start;

    /*
     * Cloning a new file under mmap_lock leads to a lock ordering conflict
     * between XFS directory reading and selinux: since this file is only
     * accessible to the user through its mapping, use S_PRIVATE flag to
     * bypass file security, in the same way as shmem_kernel_file_setup().
     */
    file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);
    if (IS_ERR(file))
        return PTR_ERR(file);

    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_vm_ops;

    if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&
            ((vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK) <
            (vma->vm_end & HPAGE_PMD_MASK)) {
        khugepaged_enter(vma, vma->vm_flags);
    }

    return 0;
}

이 v5.18 함수는 backing을 연결한 뒤, THP가 켜져 있고 VMA 내부에 정렬된 PMD-sized 구간이 있는지 검사하여 khugepaged_enter()를 호출합니다. 이 분기는 위에서 설명한 file 생성·reference 교체와 별도입니다. source의 실제 header는 "released under the GPL"이라고 되어 있으며, 그 tag의 COPYING과 GPL-2.0 원문을 함께 제공합니다. 원 파일에 없는 SPDX 표기를 원래 있던 표기처럼 추가하지 않았습니다.

v5.18 write-notify 판정과 protection 저장

vma_wants_writenotify()

공식 v5.18 mm/mmap.c, L1672–L1702 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
int vma_wants_writenotify(struct vm_area_struct *vma, pgprot_t vm_page_prot)
{
    vm_flags_t vm_flags = vma->vm_flags;
    const struct vm_operations_struct *vm_ops = vma->vm_ops;

    /* If it was private or non-writable, the write bit is already clear */
    if ((vm_flags & (VM_WRITE|VM_SHARED)) != ((VM_WRITE|VM_SHARED)))
        return 0;

    /* The backer wishes to know when pages are first written to? */
    if (vm_ops && (vm_ops->page_mkwrite || vm_ops->pfn_mkwrite))
        return 1;

    /* The open routine did something to the protections that pgprot_modify
     * won't preserve? */
    if (pgprot_val(vm_page_prot) !=
        pgprot_val(vm_pgprot_modify(vm_page_prot, vm_flags)))
        return 0;

    /* Do we need to track softdirty? */
    if (IS_ENABLED(CONFIG_MEM_SOFT_DIRTY) && !(vm_flags & VM_SOFTDIRTY))
        return 1;

    /* Specialty mapping? */
    if (vm_flags & VM_PFNMAP)
        return 0;

    /* Can the mapping track the dirty pages? */
    return vma->vm_file && vma->vm_file->f_mapping &&
        mapping_can_writeback(vma->vm_file->f_mapping);
}

앞서 설명한 hugetlb 제외 조건은 v6.0의 predicate에서 확인되는 조건입니다. 여기 수록한 v5.18은 soft-dirty 검사에서 IS_ENABLED(CONFIG_MEM_SOFT_DIRTY)와 VM_SOFTDIRTY를 직접 확인하며, 그 위치에 hugetlb를 제외하는 검사가 없습니다. 따라서 두 tag의 조건을 한 함수의 동작으로 합치면 안 됩니다. callback 검사가 PFNMAP 제외보다 앞에 있다는 순서는 이 v5.18에서도 그대로 확인할 수 있습니다.

vma_set_page_prot()

공식 v5.18 mm/mmap.c, L132–L144 · 전체 source 파일

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code    <alan@lxorguk.ukuu.org.uk>
 */
/* Linux v5.18: mm/mmap.c */
void vma_set_page_prot(struct vm_area_struct *vma)
{
    unsigned long vm_flags = vma->vm_flags;
    pgprot_t vm_page_prot;

    vm_page_prot = vm_pgprot_modify(vma->vm_page_prot, vm_flags);
    if (vma_wants_writenotify(vma, vm_page_prot)) {
        vm_flags &= ~VM_SHARED;
        vm_page_prot = vm_pgprot_modify(vm_page_prot, vm_flags);
    }
    /* remove_protection_ptes reads vma->vm_page_prot without mmap_lock */
    WRITE_ONCE(vma->vm_page_prot, vm_page_prot);
}

VM_SHARED를 지우는 대상은 local 사본이며, 마지막에는 계산된 vm_page_prot만 저장합니다. source에서 WRITE_ONCE()의 대상까지 확인하면 “VMA를 private로 바꾼다”는 잘못된 결론을 피할 수 있습니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 5 §5.0.4. 현재 구현은 Linux v6.18 mm/mmap.c, mm/vma.c, mm/shmem.c에 있습니다.