VMA / mmap 심화

Linux 커널 가상 메모리 매핑: mmap 시스템 콜 인터페이스, VMA 구조와 관리, 페이지 폴트 처리, MAP_SHARED/PRIVATE 공유 메모리, mremap/mprotect/madvise, userfaultfd, 프로세스 주소 공간 레이아웃 종합 가이드.

관련 페이지: 기본 메모리 관리는 메모리 관리 (기초), 고급 메모리 관리는 메모리 관리 (심화) 페이지를 참고하세요. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 메모리 관리 기초CPU 캐시 문서를 먼저 읽으세요. 메모리 서브시스템은 가상 메모리와 물리 메모리 정책이 동시에 동작하므로, 주소 변환과 회수 정책을 같이 보는 관점이 필요합니다.
일상 비유: 이 주제는 창고 적재와 재배치 운영과 비슷합니다. 빈 공간을 잘 배치해야 출고가 빨라지듯이, 페이지 배치/회수 정책이 성능과 지연을 직접 좌우합니다.

핵심 요약

  • mmap() — 파일, 익명 메모리, 디바이스 등을 프로세스의 가상 주소 공간에 매핑하는 시스템 콜입니다.
  • VMA (vm_area_struct) — 프로세스 주소 공간의 연속된 가상 메모리 영역을 나타내는 커널 자료구조입니다.
  • Demand Paging — mmap 시 가상 주소만 확보하고, 실제 물리 페이지는 첫 접근 시(page fault) 할당하는 지연 할당 기법입니다.
  • MAP_SHARED / MAP_PRIVATE — 공유 매핑은 모든 프로세스가 동일한 물리 페이지를 공유하고, 사유 매핑은 쓰기 시 COW(Copy-On-Write)로 복사합니다.
  • mprotect / mremap / madvise — 매핑된 영역의 보호 속성 변경, 크기 조정, 접근 힌트 제공 등 mmap 후 동적 제어 시스템 콜입니다.

단계별 이해

  1. mmap 호출 → VMA 생성mmap()을 호출하면 커널은 vm_area_struct를 생성하여 주소 범위, 권한, 파일 매핑 정보를 기록합니다.

    이 시점에는 페이지 테이블 엔트리가 없고, 가상 주소만 예약된 상태입니다.

  2. 첫 접근 → Page Fault — 프로세스가 매핑된 주소에 접근하면 page fault가 발생합니다. 커널은 VMA를 조회하여 유효한 접근인지 확인합니다.

    유효하면 물리 페이지를 할당하고 페이지 테이블에 매핑을 생성합니다. 파일 매핑이면 파일에서 데이터를 읽어옵니다.

  3. COW (Copy-On-Write) — MAP_PRIVATE 매핑에서 쓰기를 시도하면 물리 페이지를 복사하여 프로세스 전용 페이지로 분리합니다.

    fork() 후 부모-자식이 동일한 VMA를 공유하다가, 쓰기 시 비로소 복사되어 메모리를 절약합니다.

  4. 메모리 보호와 제어 — mprotect()로 실행 금지(NX), 읽기 전용 등 권한을 동적으로 변경할 수 있습니다.

    JIT 컴파일러, 샌드박스, 디버거 등이 이를 활용하며, 보안 강화(ASLR, DEP)의 핵심 메커니즘입니다.

mmap 심화 — 가상 메모리 매핑

mmap()은 프로세스의 가상 주소 공간에 메모리 영역을 매핑하는 핵심 시스템 콜입니다. 파일 I/O, 공유 메모리, 익명 메모리 할당, 디바이스 메모리 접근 등 커널의 거의 모든 메모리 관련 기능이 mmap()을 통해 구현됩니다.

mmap 시스템 콜 인터페이스

#include <sys/mman.h>

void *mmap(
    void    *addr,    /* 요청 시작 주소 (NULL이면 커널이 선택) */
    size_t   len,     /* 매핑 길이 (바이트) */
    int      prot,    /* 보호 플래그: PROT_READ|WRITE|EXEC|NONE */
    int      flags,   /* 매핑 플래그: MAP_SHARED|PRIVATE|ANONYMOUS|... */
    int      fd,      /* 파일 디스크립터 (MAP_ANONYMOUS이면 -1) */
    off_t   offset   /* 파일 내 오프셋 (PAGE_SIZE 배수) */
);
int munmap(void *addr, size_t len);
/* 커널 측 syscall 정의 (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 (offset_in_page(off))
        return -EINVAL;
    return ksys_mmap_pgoff(addr, len, prot, flags, fd,
                            off >> PAGE_SHIFT);
}

/* ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap 호출 체인 */
unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flags, vm_flags_t vm_flags,
    unsigned long pgoff, unsigned long *populate,
    struct list_head *uf);

매핑 플래그 상세

보호 플래그 (prot)

플래그설명
PROT_NONE0x0접근 불가 — guard page, 주소 공간 예약에 사용
PROT_READ0x1읽기 허용
PROT_WRITE0x2쓰기 허용 (x86에서 PROT_READ 자동 포함)
PROT_EXEC0x4실행 허용 — NX bit 지원 시 W^X 정책과 상호작용
W^X (Write XOR Execute): SELinux의 execmem 정책은 PROT_WRITE | PROT_EXEC 동시 사용을 금지합니다. JIT 컴파일러는 먼저 PROT_WRITE로 코드를 쓴 후 mprotect()PROT_EXEC로 전환하는 2단계 패턴을 사용합니다.

매핑 유형 플래그 (flags) — 필수

플래그설명COW디스크 반영
MAP_SHARED공유 매핑 — 여러 프로세스가 동일 물리 페이지 공유없음msync/munmap 시 반영
MAP_SHARED_VALIDATEMAP_SHARED + 알 수 없는 플래그 시 EOPNOTSUPP 반환없음반영
MAP_PRIVATE사적 매핑 — 쓰기 시 COW로 사본 생성있음반영 안 됨

매핑 동작 플래그 (flags) — 선택

플래그설명
MAP_ANONYMOUS0x20파일 없는 익명 매핑 — 힙 확장, 큰 메모리 할당에 사용
MAP_FIXED0x10addr에 정확히 배치 — 기존 매핑 덮어씀 (위험)
MAP_FIXED_NOREPLACE0x100000addr에 배치하되, 충돌 시 EEXIST 반환 (안전한 대안)
MAP_POPULATE0x8000매핑 시 모든 페이지를 사전 폴트 — read-ahead 효과
MAP_LOCKED0x2000mlock과 동일 — 페이지를 RAM에 고정 (스왑 방지)
MAP_NORESERVE0x4000스왑 영역 예약 없이 매핑 — overcommit 의존
MAP_GROWSDOWN0x100스택처럼 아래로 성장하는 매핑
MAP_HUGETLB0x40000Huge Page 사용 — MAP_HUGE_2MB, MAP_HUGE_1GB 조합
MAP_SYNC0x80000DAX/PMEM — 영속 메모리에 대한 동기화 보장
MAP_32BIT0x40x86-64 전용 — 하위 2GB 영역에 매핑
/* 매핑 유형별 사용 패턴 */

/* 1. 파일 매핑 (MAP_SHARED) — 여러 프로세스가 같은 파일을 공유 */
int fd = open("/data/shared.db", O_RDWR);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, 0);
/* → 쓰기가 파일에 반영됨, 다른 프로세스에서도 즉시 가시 */

/* 2. 파일 매핑 (MAP_PRIVATE) — 읽기 전용 데이터, COW */
void *priv = mmap(NULL, size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE, fd, 0);
/* → 쓰기 시 사본 생성, 원본 파일 변경 없음 */
/* → .text 세그먼트(실행 코드), 공유 라이브러리 로딩에 사용 */

/* 3. 익명 매핑 — 큰 메모리 할당 (glibc malloc > 128KB) */
void *anon = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* → 제로 페이지에 매핑, 쓰기 시 실제 물리 페이지 할당 */

/* 4. 공유 익명 매핑 — 부모-자식 프로세스 간 IPC */
void *ipc = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS, -1, 0);
fork();
/* → 부모와 자식이 같은 물리 페이지를 직접 공유 */

/* 5. Huge Page 매핑 */
void *huge = mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB,
                   -1, 0);

VMA (Virtual Memory Area) 구조체

커널은 프로세스의 가상 주소 공간을 VMA 단위로 관리합니다. 각 VMA는 동일한 속성을 가진 연속된 가상 주소 범위를 나타내며, mm_struct의 maple tree (6.1+, 이전엔 red-black tree)로 관리됩니다.

/* include/linux/mm_types.h */
struct vm_area_struct {
    unsigned long vm_start;      /* VMA 시작 주소 (포함) */
    unsigned long vm_end;        /* VMA 끝 주소 (미포함) */

    struct mm_struct *vm_mm;     /* 소속 mm_struct */
    pgprot_t vm_page_prot;       /* PTE 보호 비트 */
    vm_flags_t vm_flags;         /* VM_READ, VM_WRITE, VM_EXEC, ... */

    const struct vm_operations_struct *vm_ops; /* 폴트 핸들러 등 */

    unsigned long vm_pgoff;      /* 파일 내 페이지 오프셋 */
    struct file *vm_file;        /* 매핑된 파일 (익명이면 NULL) */
    void *vm_private_data;       /* 드라이버 전용 데이터 */
};

주요 vm_flags

플래그의미설명
VM_READ읽기 허용PROT_READ에 대응
VM_WRITE쓰기 허용PROT_WRITE에 대응
VM_EXEC실행 허용PROT_EXEC에 대응
VM_SHARED공유 매핑MAP_SHARED에 대응
VM_MAYREAD/WRITE/EXECmprotect 허용 범위mprotect()로 추가 가능한 최대 권한
VM_GROWSDOWN아래로 확장스택 VMA에 설정
VM_DONTEXPAND확장 금지mremap 확장 방지
VM_DONTCOPYfork 시 복사 금지VM_WIPEONFORK: fork 시 제로화
VM_IOI/O 메모리디바이스 메모리 매핑 — core dump 제외
VM_PFNMAPPFN 직접 매핑struct page 없는 물리 주소 매핑
VM_LOCKEDmlock됨페이지 스왑/회수 방지
VM_HUGETLBHuge pagehugetlb 매핑

Maple Tree 기반 VMA 관리 (커널 6.1+)

Linux 6.1에서 VMA 관리 자료구조가 red-black tree + linked list 조합에서 Maple Tree로 전면 교체되었습니다 (Liam R. Howlett, Oracle). Maple Tree는 B-tree 변형으로 설계되어, 범위 기반(range-based) 인덱싱에 최적화된 캐시 친화적 자료구조입니다.

교체 배경: rbtree + linked list의 한계

문제rbtree + linked list (6.0 이전)Maple Tree (6.1+)
자료구조 수rbtree + linked list + 인터벌 트리 (3개 동시 유지)단일 Maple Tree로 통합
VMA 포인터vm_next, vm_prev, vm_rb 각각 유지모든 포인터 제거 → VMA 구조체 축소
캐시 효율rbtree 노드가 메모리에 분산 → 캐시 미스 빈번노드당 최대 16개 엔트리 → 캐시 라인 활용 극대화
범위 연산gap 탐색에 augmented rbtree 필요범위 기반 인덱싱이 기본 → gap 탐색 자연스러움
RCU 호환rbtree 회전 시 RCU 안전성 보장 어려움RCU-safe 설계 (노드 교체 방식)
Lock 범위mmap_lock 전체 보유 필수per-VMA lock 도입 기반 마련 (6.4+)

Maple Tree 노드 구조

Maple Tree는 두 가지 노드 타입을 사용합니다. 내부 노드(maple_range_64)는 최대 16개의 pivot과 자식 포인터를 저장하고, 리프 노드(maple_arange_64)는 데이터 포인터(VMA 주소)를 직접 저장합니다. 각 노드는 256바이트로 정렬되어 캐시 라인 경계에 맞춥니다.

/* include/linux/maple_tree.h */
struct maple_tree {
    union {
        spinlock_t ma_lock;          /* 내부 락 */
        lockdep_map_p ma_external_lock; /* 외부 락 사용 시 */
    };
    unsigned int ma_flags;           /* 트리 플래그 (MT_FLAGS_*) */
    void __rcu *ma_root;             /* 루트 노드 포인터 */
};

/* 노드 타입 — 내부 노드 (최대 16개 피벗) */
struct maple_range_64 {
    struct maple_pnode *parent;     /* 부모 노드 */
    unsigned long pivot[MAPLE_RANGE64_SLOTS - 1];  /* 키 구간 경계 (최대 15개) */
    union {
        void __rcu *slot[MAPLE_RANGE64_SLOTS];  /* 자식/데이터 포인터 (최대 16개) */
        struct {
            void __rcu *pad[MAPLE_RANGE64_SLOTS - 1];
            struct maple_metadata meta;  /* 엔드 인덱스, gap 정보 */
        };
    };
};

/* 리프/내부 공용 — augmented 노드 (gap 추적 포함) */
struct maple_arange_64 {
    struct maple_pnode *parent;
    unsigned long pivot[MAPLE_ARANGE64_SLOTS - 1];  /* 키 구간 경계 (최대 9개) */
    void __rcu *slot[MAPLE_ARANGE64_SLOTS];           /* 자식/데이터 (최대 10개) */
    unsigned long gap[MAPLE_ARANGE64_SLOTS];           /* 각 서브트리 최대 gap 크기 */
    struct maple_metadata meta;
};
노드 크기와 캐시 효율: Maple Tree 노드는 256바이트로 고정되어 4개의 캐시 라인(64B x 4)에 정확히 맞습니다. rbtree에서는 단일 VMA 조회에 트리 깊이만큼의 캐시 라인 접근이 필요했지만, Maple Tree는 한 노드 내에서 최대 16개 엔트리를 선형 탐색하므로 프리페치(prefetch)가 효과적으로 동작합니다.

Maple Tree 동작 원리

Maple Tree는 범위 인덱싱을 기본으로 합니다. 각 엔트리는 (index, last) 범위를 가지며, VMA의 경우 vm_start가 index, vm_end - 1이 last가 됩니다. pivot 배열은 정렬된 경계값을 저장하여, slot[i]는 pivot[i-1]+1 ~ pivot[i] 범위의 데이터를 가리킵니다.

루트 (maple_arange_64) pivot[0]=0x4000 pivot[1]=0xA000 pivot[2]=ULONG_MAX slot[0] slot[1] slot[2] 리프 (범위: 0~0x4000) 0x1000-0x2FFF 0x3000-0x3FFF 리프 (0x4001~0xA000) 0x5000-0x8FFF 리프 (0xA001~MAX) 0xB000-0xCFFF 0xF000-0xFFFF VMA: [heap] VMA: [anon] VMA: [libc] VMA: [mmap] VMA: [stack] * pivot[i]는 slot[i]가 커버하는 범위의 상한값 * slot[i] → pivot[i-1]+1 ~ pivot[i] 범위의 데이터 (또는 자식 노드) * gap[i] = slot[i] 서브트리 내 가장 큰 빈 공간 크기 (arange 노드) * 빈 슬롯(NULL) = 해당 범위에 VMA 없음 → gap으로 활용

mm_struct의 Maple Tree

/* include/linux/mm_types.h */
struct mm_struct {
    struct {
        struct maple_tree mm_mt;  /* VMA를 관리하는 maple tree (6.1+) */
        unsigned long mmap_base;   /* mmap 영역 시작 (ASLR 적용) */
        unsigned long task_size;   /* 유저 주소 공간 크기 */
        int map_count;             /* VMA 개수 */
        unsigned long total_vm;    /* 총 매핑된 페이지 수 */
        unsigned long locked_vm;   /* mlock된 페이지 수 */
        unsigned long data_vm;     /* 데이터 매핑 페이지 수 */
        unsigned long stack_vm;    /* 스택 매핑 페이지 수 */
    };
    /* ... */
};

Maple State (ma_state) — 핵심 순회 인터페이스

Maple Tree의 모든 연산은 Maple State(ma_state)를 통해 수행됩니다. ma_state는 트리 내 현재 위치(커서)를 추적하며, 연속된 연산에서 탐색 경로를 재활용하여 성능을 최적화합니다.

/* include/linux/maple_tree.h */
struct ma_state {
    struct maple_tree *tree;     /* 대상 maple tree */
    unsigned long index;          /* 현재 탐색 시작 인덱스 */
    unsigned long last;           /* 현재 탐색 끝 인덱스 */
    struct maple_enode *node;    /* 현재 위치한 노드 */
    unsigned long min;            /* 현재 노드의 최소 범위 */
    unsigned long max;            /* 현재 노드의 최대 범위 */
    struct maple_alloc *alloc;   /* 사전 할당된 노드 목록 */
    unsigned char depth;          /* 현재 트리 깊이 */
    unsigned char offset;         /* 현재 노드 내 슬롯 오프셋 */
    unsigned char mas_flags;      /* 상태 플래그 */
};

/* Maple State 초기화 매크로 */
#define MA_STATE(name, mt, first, end) \
    struct ma_state name = {      \
        .tree = mt,                 \
        .index = first,             \
        .last = end,                \
        .node = MAS_START,         \
        .min = 0,                   \
        .max = ULONG_MAX,          \
        .alloc = NULL,              \
        .mas_flags = 0,            \
    }

주요 Maple Tree API

함수동작복잡도설명
mas_walk(&mas)정확한 인덱스 조회O(log n)mas.index 위치의 엔트리를 찾아 반환
mas_find(&mas, max)범위 내 다음 엔트리O(log n)현재 위치~max 범위에서 다음 non-NULL 엔트리
mas_find_rev(&mas, min)역방향 탐색O(log n)현재 위치~min 범위에서 이전 non-NULL 엔트리
mas_store(&mas, entry)엔트리 저장O(log n)[mas.index, mas.last] 범위에 entry 저장
mas_store_gfp(&mas, entry, gfp)GFP(Get Free Pages) 지정 저장O(log n)노드 할당 시 GFP 플래그 지정 가능
mas_erase(&mas)엔트리 삭제O(log n)현재 위치의 엔트리를 NULL로 설정
mas_empty_area(&mas, min, max, size)빈 공간 탐색O(log n)size 이상의 gap을 찾아 mas.index/last 설정
mas_empty_area_rev(&mas, min, max, size)역방향 gap 탐색O(log n)top-down 할당을 위한 역방향 gap 탐색
mas_prev(&mas, min)이전 엔트리O(1) 평균인접 엔트리는 같은 노드 내에서 O(1)
mas_next(&mas, max)다음 엔트리O(1) 평균순차 순회 시 캐시 친화적

VMA 관리 실전 코드

/* VMA 조회 — find_vma() 내부는 Maple Tree 기반 (mm/mmap.c) */
struct vm_area_struct *find_vma(struct mm_struct *mm,
                                unsigned long addr)
{
    struct vm_area_struct *vma;

    /* Maple Tree에서 addr 이상의 첫 VMA 탐색 */
    vma = mt_find(&mm->mm_mt, &addr, ULONG_MAX);

    return vma;
}

/* VMA 삽입 — mas_store_gfp 사용 */
static int vma_mas_store(struct vm_area_struct *vma,
                          struct ma_state *mas)
{
    mas->index = vma->vm_start;
    mas->last = vma->vm_end - 1;
    mas_store_gfp(mas, vma, GFP_KERNEL);
    return 0;
}

/* 빈 가상 주소 공간 탐색 (mmap 할당용) */
unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info)
{
    struct mm_struct *mm = current->mm;
    MA_STATE(mas, &mm->mm_mt, 0, 0);

    /* gap[i]를 활용하여 O(log n)으로 충분한 크기의 빈 공간 탐색 */
    if (mas_empty_area_rev(&mas, info->low_limit,
                           info->high_limit - 1,
                           info->length))
        return -ENOMEM;

    return mas.index;  /* 찾은 빈 공간의 시작 주소 */
}

/* VMA Iterator — 순차 순회 래퍼 (6.1+) */
struct vm_area_struct *vma;
VMA_ITERATOR(vmi, mm, 0);    /* ma_state를 래핑한 VMA 전용 이터레이터 */
for_each_vma(vmi, vma) {
    pr_info("VMA: %lx-%lx flags=%lx\\n",
            vma->vm_start, vma->vm_end, vma->vm_flags);
}

/* 특정 범위 내 VMA만 순회 */
VMA_ITERATOR(vmi, mm, start_addr);
for_each_vma_range(vmi, vma, end_addr) {
    /* start_addr ~ end_addr 범위와 겹치는 VMA만 순회 */
}
💡

VMA Iterator와 Maple State: VMA_ITERATOR는 내부적으로 ma_state를 래핑합니다. for_each_vmamas_find를 반복 호출하며, 같은 리프 노드 내의 연속 VMA 접근은 O(1)입니다. 6.0 이전의 vma = vma->vm_next linked list 순회를 대체합니다.

RCU-safe 읽기와 per-VMA Lock (6.4+)

Maple Tree의 핵심 설계 목표 중 하나는 RCU-safe 읽기입니다. 읽기 측은 rcu_read_lock()만으로 트리를 안전하게 순회할 수 있으며, 쓰기 측이 노드를 수정할 때는 기존 노드를 수정하지 않고 새 노드를 생성한 뒤 포인터를 교체(publish)합니다.

/* RCU-safe VMA 조회 (mmap_lock 없이) — 6.4+ per-VMA lock */
struct vm_area_struct *lock_vma_under_rcu(
    struct mm_struct *mm,
    unsigned long address)
{
    struct vm_area_struct *vma;

    rcu_read_lock();
    /* Maple Tree에서 RCU 보호 하에 VMA 조회 */
    vma = mt_find(&mm->mm_mt, &address, ULONG_MAX);
    if (!vma || vma->vm_start > address) {
        rcu_read_unlock();
        return NULL;
    }

    /* per-VMA lock 획득 시도 (실패 시 mmap_lock fallback) */
    if (!vma_start_read(vma)) {
        rcu_read_unlock();
        return NULL;  /* 호출자가 mmap_lock으로 재시도 */
    }
    rcu_read_unlock();
    return vma;  /* per-VMA lock 보유 상태로 반환 */
}

/* Page Fault 경로에서의 활용 (6.4+) */
/*
 * 1단계: rcu_read_lock + per-VMA lock 시도 (빠른 경로)
 * 2단계: 실패 시 mmap_read_lock() fallback (느린 경로)
 *
 * → 대부분의 page fault에서 mmap_lock 경합 없이 VMA 접근 가능
 * → 멀티스레드 워크로드에서 극적인 확장성 향상
 */
CVE-2023-3269 (StackRot)과 스택 VMA 경쟁 조건:

Linux 6.1~6.4에서 MAP_GROWSDOWN 스택 영역 자동 확장 시 발생하는 Use-After-Free 취약점입니다. expand_downwards()가 스택 VMA 하단 경계를 확장하는 과정에서, 다른 스레드가 경쟁적으로 해당 VMA를 교체·해제할 수 있었습니다. 공격자는 이 경쟁 조건을 통해 로컬 권한 상승(LPE)을 달성할 수 있었습니다. Linux 6.4.1에서 수정되었으며, 이후 스택 VMA 확장의 동기화가 강화되었습니다.

Maple Tree의 범용 활용

Maple Tree는 VMA 관리 외에도 커널 내 다양한 범위 기반 인덱싱에 활용됩니다.

사용처인덱스 키저장 값도입 버전
VMA 관리 (mm_struct)가상 주소vm_area_struct *6.1
PID 할당 (idr 대체)PID 번호task_struct *6.1
regmap 캐시 (REGCACHE_MAPLE)레지스터 주소레지스터 값6.4
파일 페이지 캐시 (계획)파일 오프셋struct folio *논의 중
6.1 이전 vs 이후 요약: Linux 6.1에서 VMA 관리가 red-black tree + linked list에서 Maple Tree로 전환되었습니다. vm_area_struct에서 vm_next, vm_prev, vm_rb 필드가 모두 제거되어 구조체 크기가 줄었고, 6.4에서는 이를 기반으로 per-VMA lock이 도입되어 page fault 경로에서 mmap_lock 경합을 획기적으로 줄였습니다.

do_mmap 내부 처리 흐름

mmap() 시스템 콜이 커널 내부에서 처리되는 과정을 단계별로 살펴봅니다.

mmap() 내부 처리 흐름 userspace: mmap(addr, len, prot, flags, fd, off) ksys_mmap_pgoff() → vm_mmap_pgoff() security_mmap_file() LSM 보안 검사 후 do_mmap() 진입 get_unmapped_area() 파일 매핑: f_op->get_unmapped_area() 익명 매핑: arch_get_unmapped_area_topdown() 가상 주소 영역 결정 mmap_region() vma_merge() (인접/동일 속성 병합) vm_area_alloc() (신규 VMA 할당) file->f_op->mmap() (파일 매핑 시) vma_link()로 maple tree에 삽입 옵션 처리 MAP_POPULATE면 선행 fault/populate 수행 populate 포인터에 후속 작업 정보 전달 반환: 매핑된 가상 주소 또는 음수 에러 코드
/* mm/mmap.c — do_mmap 핵심 로직 (간략화) */
unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flags, vm_flags_t vm_flags,
    unsigned long pgoff, unsigned long *populate,
    struct list_head *uf)
{
    struct mm_struct *mm = current->mm;

    /* 1. 길이 정렬 및 오버플로 검사 */
    len = PAGE_ALIGN(len);
    if (!len || len > TASK_SIZE)
        return -ENOMEM;

    /* 2. vm_flags 계산 (prot + flags → VM_READ|VM_WRITE|...) */
    vm_flags |= calc_vm_prot_bits(prot, 0) |
                calc_vm_flag_bits(flags);

    /* 3. MAP 개수 제한 확인 (/proc/sys/vm/max_map_count) */
    if (mm->map_count > sysctl_max_map_count)
        return -ENOMEM;

    /* 4. 적절한 가상 주소 탐색 */
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    /* 5. 실제 매핑 생성 */
    addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);

    /* 6. MAP_POPULATE 처리 — 사전 페이지 폴트 */
    if (populate)
        *populate = (flags & MAP_POPULATE) ? len : 0;

    return addr;
}
max_map_count: /proc/sys/vm/max_map_count (기본값 65530)은 프로세스당 VMA 최대 개수를 제한합니다. 많은 공유 라이브러리를 로드하거나, JVM처럼 다수의 mmap을 사용하는 애플리케이션에서 이 한계에 도달할 수 있습니다. sysctl -w vm.max_map_count=262144로 조정 가능합니다.

mmap 페이지 폴트 처리

mmap()lazy allocation을 사용합니다. 매핑 시점에 물리 메모리를 할당하지 않고, 실제 접근 시 page fault를 통해 물리 페이지를 할당합니다.

Page Fault 처리 흐름 *ptr = 42; → MMU → #PF (Page Fault 예외) do_page_fault() → handle_mm_fault() __handle_mm_fault() → handle_pte_fault() PTE 없음 (첫 접근) 파일 매핑 → do_fault() 읽기 → do_read_fault() → filemap_fault() 쓰기 → do_cow_fault() → COW 사본 공유쓰기 → do_shared_fault() 익명 매핑 → do_anonymous_page() 제로 페이지 할당 후 CoW 설정 PTE 존재 + !present (스왑 또는 마이그레이션) do_swap_page() → 스왑 I/O do_numa_page() → NUMA 마이그레이션 결과: 페이지 테이블 업데이트 PTE 설정 → TLB flush → 사용자 프로세스 재시작 OOM 또는 SIGSEGV: 매핑 없는 주소 접근 시

vm_operations_struct — 폴트 핸들러 콜백

/* include/linux/mm.h */
struct vm_operations_struct {
    void (*open)(struct vm_area_struct *vma);
    void (*close)(struct vm_area_struct *vma);

    /* 핵심: 페이지 폴트 시 호출 */
    vm_fault_t (*fault)(struct vm_fault *vmf);

    /* 공유 쓰기 가능 매핑에서 쓰기 시 호출 (page_mkwrite) */
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);

    /* DAX(PMEM) PFN 폴트 */
    vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);

    /* huge page 폴트 */
    vm_fault_t (*huge_fault)(struct vm_fault *vmf,
                             unsigned int order);
};

/* vm_fault 구조체 — 폴트 핸들러에 전달되는 컨텍스트 */
struct vm_fault {
    struct vm_area_struct *vma;  /* 폴트 발생 VMA */
    unsigned int flags;          /* FAULT_FLAG_WRITE 등 */
    pgoff_t pgoff;               /* 파일 내 페이지 오프셋 */
    unsigned long address;       /* 폴트 주소 (페이지 정렬) */
    struct page *page;           /* 핸들러가 채우는 결과 페이지 */
};

filemap_fault — 파일 매핑 폴트의 핵심

/* mm/filemap.c — 파일 매핑 페이지 폴트 (간략화) */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    struct file *file = vmf->vma->vm_file;
    struct address_space *mapping = file->f_mapping;
    struct folio *folio;

    /* 1. 페이지 캐시에서 folio 검색 */
    folio = filemap_get_folio(mapping, vmf->pgoff);
    if (IS_ERR(folio)) {
        /* 2. 캐시 미스 → 디스크에서 읽기 (Major Fault) */
        folio = filemap_alloc_folio(vmf->gfp_mask, 0);
        filemap_read_folio(file, mapping->a_ops->read_folio,
                          folio);
        /* → I/O 대기 발생 → major fault로 카운트 */
    }
    /* 3. 캐시 히트 → minor fault (I/O 없음) */

    vmf->page = folio_file_page(folio, vmf->pgoff);
    return VM_FAULT_LOCKED;
}
Major vs Minor Fault: Minor fault는 페이지가 이미 메모리(페이지 캐시)에 있어 디스크 I/O 없이 처리됩니다 (수 μs). Major fault는 디스크에서 읽어와야 하므로 수 ms가 소요됩니다. /proc/[pid]/stat의 minflt, majflt 필드로 프로세스별 폴트 횟수를 확인할 수 있습니다.

mprotect — 매핑 보호 속성 변경

int mprotect(void *addr, size_t len, int prot);

/* JIT 컴파일러 패턴: Write → Exec 전환 */
void *code = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(code, jit_output, code_len);    /* 코드 쓰기 */
mprotect(code, page_size, PROT_READ | PROT_EXEC); /* W→X 전환 */
((void(*)())code)();                    /* 실행 */

/* 커널: mprotect → do_mprotect_pkey() → VMA 분할/병합 + PTE 갱신 */
/* VMA의 vm_flags 변경 → 페이지 테이블 walk → PTE 보호 비트 갱신 */
/* TLB flush 필요 (다른 CPU에도 전파) */

mremap — 매핑 크기 변경/이동

void *mremap(void *old_addr, size_t old_size,
             size_t new_size, int flags, ...);

/* 확장 — 인접 공간이 비어있으면 제자리 확장, 아니면 이동 */
void *new_ptr = mremap(old_ptr, old_size, new_size, MREMAP_MAYMOVE);
/* → glibc realloc()이 내부적으로 사용 */
/* → 이동 시 페이지 테이블 엔트리만 재배치 (데이터 복사 없음) */

/* 고정 주소로 이동 */
void *moved = mremap(old_ptr, old_size, new_size,
                      MREMAP_MAYMOVE | MREMAP_FIXED, new_addr);

/* 커널 경로: mremap → do_mremap()
 *   → 축소: do_munmap() 으로 끝부분 제거
 *   → 확장: vma_merge() 시도 → 실패 시 move_vma()
 *   → move_vma(): 새 VMA 생성 + move_page_tables() (PTE 이동)
 */

madvise — 매핑 접근 패턴 힌트

int madvise(void *addr, size_t len, int advice);
advice동작용도
MADV_NORMAL기본 read-ahead 정책일반 접근
MADV_SEQUENTIAL공격적 read-ahead, 지나간 페이지 조기 회수순차 파일 처리
MADV_RANDOMread-ahead 비활성화DB 인덱스 랜덤 접근
MADV_WILLNEED비동기 prefetch (readahead)곧 접근할 데이터 사전 로드
MADV_DONTNEED페이지 즉시 해제 (재접근 시 재폴트)메모리 해제, GC
MADV_FREElazy 해제 — 메모리 압박 시에만 회수malloc free pool (4.5+)
MADV_HUGEPAGETHP 사용 권장대용량 힙, DB 버퍼풀
MADV_NOHUGEPAGETHP 사용 금지latency 민감한 워크로드
MADV_MERGEABLEKSM 대상으로 등록VM 중복 페이지 병합
MADV_COLD페이지를 inactive 리스트로 이동우선순위 낮은 캐시 (5.4+)
MADV_PAGEOUT페이지를 스왑으로 강제 이동프로액티브 메모리 회수 (5.4+)
/* 실전 예: DB 엔진의 버퍼풀 관리 */
void *buf = mmap(NULL, POOL_SIZE, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(buf, POOL_SIZE, MADV_HUGEPAGE);   /* THP 활성화 */

/* 특정 영역을 곧 사용할 예정 */
madvise(hot_region, HOT_SIZE, MADV_WILLNEED);

/* 사용 완료된 영역 해제 */
madvise(cold_region, COLD_SIZE, MADV_DONTNEED);

/* MADV_FREE vs MADV_DONTNEED:
 * DONTNEED: 즉시 페이지 해제 → 재접근 시 제로 페이지
 * FREE:     lazy 해제 → 메모리 충분하면 기존 데이터 유지 (더 빠름)
 */

msync — 매핑 데이터 디스크 동기화

int msync(void *addr, size_t len, int flags);
/* flags: MS_SYNC (동기 flush), MS_ASYNC (비동기), MS_INVALIDATE */

/* MAP_SHARED 파일 매핑 데이터 보장 */
memcpy(mapped_data + offset, new_data, len);
msync(mapped_data + offset, len, MS_SYNC);
/* → dirty 페이지를 디스크에 flush → writeback 완료까지 블록 */

/* 커널: msync → vfs_fsync_range() → 파일시스템별 fsync 호출 */

mlock / mlockall — 페이지 고정

int mlock(const void *addr, size_t len);
int mlock2(const void *addr, size_t len, unsigned int flags);
int mlockall(int flags);  /* MCL_CURRENT, MCL_FUTURE, MCL_ONFAULT */

/* 실시간 애플리케이션: 스왑에 의한 지연 방지 */
mlockall(MCL_CURRENT | MCL_FUTURE);
/* → 현재 + 미래 모든 매핑을 RAM에 고정 */
/* → RLIMIT_MEMLOCK에 의해 제한 (CAP_IPC_LOCK으로 해제) */

/* mlock2 (4.4+): MLOCK_ONFAULT — 접근 시점에만 잠금 */
mlock2(addr, len, MLOCK_ONFAULT);
/* → 매핑 전체를 사전 폴트하지 않고, 폴트 발생 시에만 잠금 */

디바이스 드라이버 mmap 구현

디바이스 드라이버는 file_operations.mmap 콜백을 통해 디바이스 메모리(MMIO)나 DMA 버퍼를 사용자 공간에 직접 매핑할 수 있습니다.

/* 디바이스 드라이버 mmap 콜백 기본 구조 */
static int my_dev_mmap(struct file *filp,
                       struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn = dev->phys_addr >> PAGE_SHIFT;

    /* 크기 검증 */
    if (size > dev->mem_size)
        return -EINVAL;

    /* I/O 메모리 매핑: uncacheable 설정 */
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    /* 물리 주소를 사용자 가상 주소에 매핑 */
    if (remap_pfn_range(vma, vma->vm_start, pfn,
                        size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .mmap    = my_dev_mmap,
    /* ... */
};

remap_pfn_range vs vm_insert_page

함수대상struct page 필요용도
remap_pfn_range()연속 물리 주소불필요MMIO, 연속 DMA 버퍼
io_remap_pfn_range()I/O 물리 주소불필요PCI BAR 등 I/O 메모리
vm_insert_page()개별 페이지필요커널 할당 페이지 (kmalloc 등)
vmf_insert_pfn()PFN불필요fault handler에서 PFN 직접 삽입
dma_mmap_coherent()DMA 버퍼내부 관리dma_alloc_coherent() 버퍼 매핑
/* DMA 버퍼를 사용자 공간에 매핑 */
static int my_dma_mmap(struct file *filp,
                       struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;

    return dma_mmap_coherent(dev->dev,
                             vma,
                             dev->dma_vaddr,   /* 커널 가상 주소 */
                             dev->dma_handle,   /* DMA 물리 주소 */
                             dev->dma_size);
}

/* 폴트 기반 매핑 — 필요한 페이지만 점진적으로 매핑 */
static vm_fault_t my_fault(struct vm_fault *vmf)
{
    struct my_device *dev = vmf->vma->vm_private_data;
    unsigned long pfn = dev->phys_addr >> PAGE_SHIFT;

    pfn += vmf->pgoff;  /* 오프셋 계산 */

    return vmf_insert_pfn(vmf->vma, vmf->address, pfn);
}

static const struct vm_operations_struct my_vm_ops = {
    .fault = my_fault,
};

static int my_fault_mmap(struct file *filp,
                         struct vm_area_struct *vma)
{
    vma->vm_ops = &my_vm_ops;
    vma->vm_private_data = filp->private_data;
    vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND;
    return 0;
}
보안 주의: 디바이스 드라이버 mmap 구현 시 반드시 매핑 범위를 검증해야 합니다. 사용자가 요청한 vm_pgoff와 크기가 디바이스 메모리 범위를 초과하지 않는지 확인하지 않으면, 임의 물리 메모리 접근으로 이어지는 권한 상승 취약점이 발생합니다.

프로세스 주소 공간 레이아웃

x86-64 프로세스의 가상 주소 공간에서 mmap 영역의 위치와 역할을 확인합니다.

x86-64 프로세스 가상 주소 공간 (128TB 유저 영역) 주소 커널 공간 (128TB) 커널 코드, 데이터, vmalloc, physmap FFFF_8000 _0000_0000 Canonical Hole (비표준 영역 — 하드웨어 제약) 7FFF_FFFF _FFFF Stack ↓ (성장 방향: 하향) RLIMIT_STACK 제한, argv/envp/auxv + guard page mmap 영역 ↓ (성장 방향: 하향) 공유 라이브러리 (libc.so, ld-linux.so, libpthread.so) MAP_ANONYMOUS, MAP_SHARED 파일 매핑 Heap ↑ (성장 방향: 상향) malloc → brk() / mmap(MAP_ANONYMOUS) 사용 ELF 세그먼트 (고정 주소 or ASLR) BSS (미초기화) Data (초기화) Text (코드, RX) 0000_0000 _0000_0000 0x0 (NULL — 접근 시 SIGSEGV) 주요 레지스터/정보: ASLR: 스택/mmap/힙 기반 주소 무작위화 KASLR: 커널 공간도 무작위화 cat /proc/self/maps 로 확인 가능 total: 유저 128TB + 커널 128TB = 256TB 5-level paging: 유저 64PB (Linux 6.x) VMA: vm_area_struct의 rb-tree로 관리
# 프로세스의 VMA 목록 확인
cat /proc/self/maps
# 주소범위             권한  오프셋   장치   inode  경로
# 55a3b2400000-55a3b2428000 r--p 00000000 fd:01 1234  /usr/bin/bash
# 55a3b2428000-55a3b24f0000 r-xp 00028000 fd:01 1234  /usr/bin/bash
# 55a3b2600000-55a3b2610000 rw-p 00000000 00:00 0     [heap]
# 7f1a3c000000-7f1a3c021000 rw-p 00000000 00:00 0     (anonymous)
# 7f1a3d200000-7f1a3d3c0000 r--p 00000000 fd:01 5678  /lib/libc.so.6
# 7ffc12300000-7ffc12321000 rw-p 00000000 00:00 0     [stack]
# 7ffc123fe000-7ffc12400000 r--p 00000000 00:00 0     [vvar]
# 7ffc12400000-7ffc12401000 r-xp 00000000 00:00 0     [vdso]

# 상세 VMA 정보 (/proc/[pid]/smaps)
cat /proc/self/smaps
# 7f1a3c000000-7f1a3c021000 rw-p 00000000 00:00 0
# Size:               132 kB
# Rss:                 80 kB    ← 실제 물리 메모리 사용량
# Pss:                 80 kB    ← 공유 비례 크기
# Shared_Clean:         0 kB
# Shared_Dirty:         0 kB
# Private_Clean:        0 kB
# Private_Dirty:       80 kB
# Referenced:          80 kB
# Anonymous:           80 kB
# LazyFree:             0 kB
# VmFlags: rd wr mr mw me ac sd

특수 매핑

vDSO / vvar — 커널→사용자 공유 매핑

/* vDSO (virtual Dynamic Shared Object):
 * 커널이 모든 프로세스에 자동 매핑하는 가상 공유 라이브러리.
 * gettimeofday(), clock_gettime(), getcpu() 등을
 * 시스템 콜 없이 사용자 공간에서 직접 실행.
 */

/* vvar: vDSO가 참조하는 커널 데이터 페이지 (읽기 전용) */
/* vsyscall_gtod_data, tk_fast_mono 등 시간 데이터 포함 */

/* arch/x86/entry/vdso/vma.c */
static int map_vdso(const struct vdso_image *image,
                    unsigned long addr)
{
    /* vvar 영역 매핑 (읽기 전용 데이터) */
    _install_special_mapping(mm, addr, -image->sym_vvar_start,
                            VM_READ | VM_MAYREAD, &vvar_mapping);

    /* vDSO 코드 매핑 (읽기+실행) */
    _install_special_mapping(mm, text_start, image->size,
                            VM_READ | VM_EXEC | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC,
                            &vdso_mapping);
}

userfaultfd — 사용자 공간 폴트 처리

/* userfaultfd: 페이지 폴트를 사용자 공간에서 처리 (4.3+)
 * 용도: VM 라이브 마이그레이션, 사용자 공간 스왑, CRIU(checkpoint/restore)
 */
int uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);

/* 모니터링할 매핑 등록 */
struct uffdio_register reg = {
    .range = { .start = (unsigned long)addr, .len = size },
    .mode = UFFDIO_REGISTER_MODE_MISSING,  /* 미할당 페이지 폴트 */
};
ioctl(uffd, UFFDIO_REGISTER, &reg);

/* 폴트 이벤트 수신 (poll/epoll 가능) */
struct uffd_msg msg;
read(uffd, &msg, sizeof(msg));
/* msg.arg.pagefault.address → 폴트 주소 */

/* 페이지 공급 */
struct uffdio_copy copy = {
    .dst = msg.arg.pagefault.address,
    .src = (unsigned long)page_data,
    .len = 4096,
};
ioctl(uffd, UFFDIO_COPY, &copy);
/* → 커널이 페이지를 매핑하고 폴트를 해제 */

mmap 성능 최적화

시나리오문제최적화
대용량 파일 순차 읽기minor fault 누적MAP_POPULATE + MADV_SEQUENTIAL
DB 랜덤 I/O불필요한 read-aheadMADV_RANDOM
대용량 익명 매핑TLB 미스MAP_HUGETLB 또는 MADV_HUGEPAGE
실시간 시스템스왑에 의한 지연MAP_LOCKED 또는 mlockall()
메모리 할당/해제 반복VMA 단편화MADV_FREE (해제 대신 lazy reclaim)
VM 라이브 마이그레이션다운타임userfaultfd로 점진적 전송
NUMA 노드 미스원격 메모리 접근mbind() / set_mempolicy()
/* 성능 모니터링 */

/* 1. 프로세스별 폴트 통계 */
/* /proc/[pid]/stat → field 10(minflt), 12(majflt) */

/* 2. 시스템 전체 */
/* perf stat -e page-faults,minor-faults,major-faults ./app */

/* 3. ftrace로 폴트 추적 */
/* echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable */

/* 4. VMA 개수 모니터링 */
/* grep VmPTE /proc/[pid]/status  ← 페이지 테이블 크기 */
/* wc -l /proc/[pid]/maps         ← VMA 개수 */

실습: perf/ftrace로 mmap 성능 분석

실제 애플리케이션의 mmap 성능을 측정하는 구체적인 예제입니다.

# 1. 페이지 폴트 이벤트 측정 (perf)
$ perf stat -e page-faults,minor-faults,major-faults \
            -e dTLB-load-misses,dTLB-store-misses \
            ./my_app

 Performance counter stats for './my_app':

         12,345      page-faults              #  123.450 K/sec
         12,280      minor-faults             #  99.5% (캐시 히트)
             65      major-faults             #   0.5% (디스크 I/O)
        234,567      dTLB-load-misses         #  TLB 미스율 분석

# 2. mmap 시스템 콜 추적 (strace)
$ strace -e mmap,munmap,mprotect,madvise -c ./my_app

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 45.23    0.000234          12        19           mmap
 32.10    0.000166           9        18           munmap
 22.67    0.000117          29         4           mprotect

# 3. ftrace로 페이지 폴트 세부 추적
$ echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ ./my_app
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace

# 출력 예시:
  my_app-1234  [000] ....  1234.567890: page_fault_user: \
    address=0x7f1234567000 ip=0x400abc error_code=0x6 (WRITE|USER)
  my_app-1234  [000] ....  1234.567923: page_fault_user: \
    address=0x7f1234568000 ip=0x400abc error_code=0x6

# 4. BPF로 실시간 폴트 분석 (bpftrace)
$ bpftrace -e 'kprobe:handle_mm_fault {
    @faults[comm] = count();
    @latency[comm] = hist(nsecs);
  }'

# Ctrl-C 후 출력:
@faults[my_app]: 12345

@latency[my_app]:
[256, 512)            3421 |@@@@@@@@@@@@@@                        |
[512, 1K)             6789 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@        |
[1K, 2K)              2135 |@@@@@@@@@                               |
mmap vs read/write 성능 비교: mmap()은 커널↔사용자 공간 데이터 복사를 제거하므로 대용량 파일 랜덤 접근에서 read()/write()보다 유리합니다. 반면, 소규모 순차 읽기에서는 read()의 VFS 최적화(read-ahead)가 더 효율적일 수 있습니다. 매핑 생성/해제의 오버헤드(VMA 할당, 페이지 테이블 구성, TLB flush)도 고려해야 합니다.

보안: VMA/mmap 관련 취약점 사례

VMA와 mmap 구현의 버그는 권한 상승(privilege escalation)이나 임의 메모리 접근으로 이어질 수 있어, 커널 보안의 핵심 영역입니다. 대표적인 CVE 사례를 통해 안전한 드라이버 및 애플리케이션 개발 방법을 학습할 수 있습니다.

CVE-2016-5195: Dirty COW (Copy-On-Write 경쟁 조건)

영향: Linux 2.6.22 (2007) ~ 4.8.3 (2016) — 약 9년간 존재한 권한 상승 취약점

원인: do_wp_page()의 COW 처리와 get_user_pages()의 경쟁 조건(race condition). 두 스레드가 동시에 실행될 때 읽기 전용 매핑에 쓰기가 가능해집니다.

/* 취약점 악용 시나리오 */
/* Thread 1: madvise(MADV_DONTNEED) 반복 — PTE 제거 */
while (1) {
    madvise(map, size, MADV_DONTNEED);
    /* → PTE를 제거하여 다음 접근 시 새로운 폴트 발생 유도 */
}

/* Thread 2: write() 시스템 콜 반복 — 읽기 전용 파일에 쓰기 시도 */
while (1) {
    lseek(fd, offset, SEEK_SET);
    write(fd, payload, size);
    /* → get_user_pages()가 COW를 건너뛰고 원본 페이지에 직접 쓰기 */
}

/* 결과: /etc/passwd 등 읽기 전용 파일 변조 가능 → root 권한 획득 */

패치: get_user_pages()에서 COW 페이지 감지 시 재시도 강제

/* mm/gup.c — 패치 후 (간략화) */
static int faultin_page(struct vm_area_struct *vma, ...)
{
    unsigned int fault_flags = FAULT_FLAG_ALLOW_RETRY;

    if (*flags & FOLL_WRITE)
        fault_flags |= FAULT_FLAG_WRITE;
+   if (*flags & FOLL_FORCE)
+       fault_flags |= FAULT_FLAG_TRIED;  /* COW 재시도 방지 */

    return handle_mm_fault(vma, address, fault_flags, NULL);
}

CVE-2023-0179: Netfilter nft_set_pipapo 잘못된 범위 검증

영향: Linux 5.6 ~ 6.1.7 — 로컬 권한 상승 (CVSS 7.8)

원인: 커널 모듈의 mmap 핸들러가 사용자 요청 범위를 검증하지 않아, 임의 커널 메모리를 사용자 공간에 매핑 가능

/* 취약한 드라이버 패턴 (예시) */
static int bad_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long pfn = vma->vm_pgoff;  /* 사용자 제공 오프셋 */
    unsigned long size = vma->vm_end - vma->vm_start;

    /* ❌ 잘못됨: 범위 검증 없음 */
    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                          vma->vm_page_prot);
    /* → 공격자가 pfn에 커널 메모리 주소를 지정하면 매핑됨 */
}

올바른 구현:

static int safe_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

    /* ✅ 필수: 범위 검증 */
    if (offset + size > dev->mem_size)
        return -EINVAL;
    if (offset & ~PAGE_MASK)
        return -EINVAL;

    /* ✅ 필수: 디바이스 메모리 범위 내로 제한 */
    unsigned long pfn = (dev->phys_base + offset) >> PAGE_SHIFT;

    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                          pgprot_noncached(vma->vm_page_prot));
}

CVE-2022-0847: mremap 크기 검증 우회

영향: Linux 5.8 ~ 5.16.11 — 권한 상승 (CVSS 7.8)

원인: mremap()MREMAP_DONTUNMAP 플래그 처리 시 이전 매핑이 해제되지 않아, 동일한 물리 페이지를 두 개의 VMA로 매핑 가능

/* 악용 예시 */
void *map1 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

/* mremap으로 동일한 물리 페이지를 두 번 매핑 */
void *map2 = mremap(map1, PAGE_SIZE, PAGE_SIZE,
                     MREMAP_MAYMOVE | MREMAP_DONTUNMAP, NULL);

/* map1과 map2가 동일한 물리 페이지를 가리킴 */
/* → 한쪽을 읽기 전용 pipe 버퍼로 splice하고, 다른 쪽으로 쓰기 */
/* → Dirty Pipe 취약점과 유사한 권한 상승 */

VMA/mmap 보안 체크리스트

영역검증 항목위험
드라이버 mmapvm_pgoff + size 범위 검증임의 물리 메모리 접근
권한 검사PROT_WRITE + MAP_SHARED 조합 시 파일 쓰기 권한 확인읽기 전용 파일 변조
정수 오버플로offset + size 계산 시 오버플로 검사범위 검증 우회
경쟁 조건멀티스레드 접근 시 적절한 락 사용TOCTOU (Time-Of-Check-Time-Of-Use)
VMA 플래그VM_IO, VM_PFNMAP, VM_DONTEXPAND 설정예상치 못한 VMA 확장/병합
캐시 일관성DMA 버퍼는 pgprot_noncached() 또는 dma_mmap_coherent() 사용데이터 손상
참고: 최신 CVE 목록은 CVE Database에서 "linux kernel mmap" 검색으로 확인 가능합니다. 커널 보안 패치는 kernel.org의 stable/longterm 릴리스 노트를 참고하세요.

mmap/페이지 폴트 디버깅 루틴

mmap 문제는 권한, 정렬, 매핑 플래그, 드라이버 remap 경로 중 하나에서 주로 발생합니다. fault 유형(minor/major/SIGBUS/SIGSEGV)을 먼저 분류하면 진단 속도가 빨라집니다.

증상우선 점검대응
SIGSEGV주소 범위/권한VMA 권한 및 경계 체크
SIGBUS파일/디바이스 backing매핑 길이/오프셋/디바이스 범위 점검
major fault 급증working set vs 메모리 압박readahead/lock/populate 전략 재검토
# fault/매핑 점검
cat /proc/<pid>/maps | head
cat /proc/<pid>/smaps | head -n 120
perf stat -e page-faults,minor-faults,major-faults ./workload

VMA/mmap과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.