Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# mprotect 🛡️

관련 소스: mm/mprotect.c, include/linux/mman.h, include/linux/mm.h

개요 (Overview)

mprotect는 프로세스의 가상 메모리 영역(VMA)에 대한 보호 속성(읽기/쓰기/실행)을 런타임에 변경하는 시스템 호출이다. Linux 7.0에서 mprotect는 단순한 PTE 보호 변경을 넘어 NUMA 밸런싱, userfaultfd-wp(write-protect), softdirty 트래킹, memory sealing 등 다양한 서브시스템과 깊이 통합되어 있다. 핵심 경로는 do_mprotect_pkey()mprotect_fixup()change_protection() → 페이지 테이블 계층(PGD→P4D→PUD→PMD→PTE) 순회이다.

소스 파일은 mm/mprotect.c(1007줄) 하나로, 시스템 호출 정의(SYSCALL_DEFINE3)부터 PTE 배치(batching) 최적화, HugePMD/HugePUD 분할, PROT_NONE 권한 검증까지 모두 포함한다. pkey_mprotect/pkey_alloc/pkey_free 시스템 호출도 함께 정의되어 있어 메모리 보호 키(per-process protection key) 지원도 담당한다.

책장에 붙은 출입 규칙을 바꾸는 일로 보면 이해가 쉽다. 책장(VMA)은 그대로 두고, "읽기만", "쓰기 허용", "실행 허용" 같은 표지판만 바꾸는 작업이 mprotect이고, 실제 책(PTE)은 필요할 때만 다시 정리된다.

# 소스 파일 경로
mm/mprotect.c                  ← 메인 소스 (1007줄)
include/linux/mman.h           ← calc_vm_prot_bits, map_deny_write_exec
include/linux/mm.h             ← VM_* 플래그, MM_CP_* 플래그, can_change_pte_writable
include/linux/mm_types.h       ← struct vm_area_struct
mm/internal.h                  ← folio_can_map_prot_numa
include/linux/pgtable.h        ← modify_prot_start_ptes, modify_prot_commit_ptes

mprotect 호출 흐름
mprotect 자료구조 관계도

빠른 점검 명령

# 1. 특정 프로세스의 VMA 권한과 pkey 확인
grep -nE '^VmFlags:|^ProtectionKey:' /proc/<pid>/smaps | head -40

# 2. mprotect 시스템 호출 카운트 확인
perf stat -e 'syscalls:sys_enter_mprotect' -p <pid> sleep 5

# 3. mprotect 관련 strace 추적
strace -e mprotect -p <pid>

# 4. 현재 메모리 보호 상태 (rwxp 등)
cat /proc/<pid>/maps | head -20

# 5. pkey가 붙은 매핑 확인
grep -E '^ProtectionKey:' /proc/<pid>/smaps | sort | uniq -c

# 6. 페이지 테이블 사용량 추적
cat /proc/<pid>/status | grep VmPTE

# 7. mprotect 호출 시 트레이싱 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_mprotect/enable

# 8. pkey_mprotect 호출 빈도 확인
perf stat -e 'syscalls:sys_enter_pkey_mprotect' -p <pid> sleep 5

# 9. 메모리 보호 관련 커널 로그
dmesg | grep -i "mprotect\|pkey\|protection"

# 10. x86에서 MDWE(Memory Deny Write Execute) 상태 확인
cat /proc/<pid>/status | grep -i mdwe

핵심 자료구조

struct vm_area_struct (선택 필드)

// include/linux/mm_types.h:913
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에 사용될 보호 속성
    union {
        const vm_flags_t vm_flags;   // VM_READ, VM_WRITE, VM_EXEC 등
        vma_flags_t flags;
    };
    const struct vm_operations_struct *vm_ops; // mprotect 콜백 (파일 시스템별)
    struct anon_vma *anon_vma;       // 익명 매핑용 anon_vma
    unsigned long vm_pgoff;          // 오프셋 (PAGE_SIZE 단위)
    struct file *vm_file;            // 매핑된 파일 (없으면 NULL)
    struct mempolicy *vm_policy;     // NUMA 정책 (CONFIG_NUMA)
};

* 파일: include/linux/mm_types.h:913-986 (핵심 필드)

VM_* 플래그 — mprotect 관련 핵심

// include/linux/mm.h:402-545 (선택)
#define VM_READ       INIT_VM_FLAG(READ)      // 읽기 가능
#define VM_WRITE      INIT_VM_FLAG(WRITE)     // 쓰기 가능
#define VM_EXEC       INIT_VM_FLAG(EXEC)      // 실행 가능
#define VM_SHARED     INIT_VM_FLAG(SHARED)    // 공유 매핑
#define VM_MAYREAD    INIT_VM_FLAG(MAYREAD)   // 읽기 허용 가능
#define VM_MAYWRITE   INIT_VM_FLAG(MAYWRITE)  // 쓰기 허용 가능
#define VM_MAYEXEC    INIT_VM_FLAG(MAYEXEC)   // 실행 허용 가능
#define VM_LOCKED     INIT_VM_FLAG(LOCKED)    // 고정 (스왑 아웃 안 됨)
#define VM_SEALED     INIT_VM_FLAG(SEALED)    // 봉인 (mprotect 거부)
#define VM_PFNMAP     INIT_VM_FLAG(PFNMAP)    // 커널 직접 매핑
#define VM_ACCOUNT    INIT_VM_FLAG(ACCOUNT)   // 오버커밋 계정

* 파일: include/linux/mm.h:402-545

* mprotect에서 VM_ACCESS_FLAGS(VM_READ|VM_WRITE|VM_EXEC)가 VMA에 설정됨

* VM_SEALED가 설정된 VMA는 mprotect가 -EPERM으로 거부 (mseal 연동)

MM_CP_* 플래그 — change_protection 제어

// include/linux/mm.h:3031-3038
#define MM_CP_TRY_CHANGE_WRITABLE  (1UL << 0)  // 쓰기 가능한 PTE로 변경 시도
#define MM_CP_PROT_NUMA            (1UL << 1)  // NUMA 밸런싱용 PROT_NONE
#define MM_CP_UFFD_WP              (1UL << 2)  // userfaultfd 쓰기 보호
#define MM_CP_UFFD_WP_RESOLVE      (1UL << 3)  // userfaultfd 쓰기 보호 해제
#define MM_CP_UFFD_WP_ALL          (MM_CP_UFFD_WP | MM_CP_UFFD_WP_RESOLVE)

* 파일: include/linux/mm.h:3031-3038

* MM_CP_TRY_CHANGE_WRITABLE: vma_wants_manual_pte_write_upgrade()가 true일 때 설정

* MM_CP_PROT_NUMA: NUMA 밸런싱이 PROT_NONE으로 페이지를 비활성화할 때 사용

can_change_pte_writable — PTE 쓰기 변경 조건

// mm/mprotect.c:97-104
bool can_change_pte_writable(struct vm_area_struct *vma, unsigned long addr,
                             pte_t pte)
{
    if (!(vma->vm_flags & VM_SHARED))
        return can_change_private_pte_writable(vma, addr, pte);  // 비공유: anon exclusive만
    return can_change_shared_pte_writable(vma, pte);             // 공유: dirty면 허용
}

* 파일: mm/mprotect.c:97-104

* MAP_PRIVATE: PageAnon(page) && PageAnonExclusive(page)인 경우만 쓰기 가능

* MAP_SHARED: pte_dirty(pte)이면 writenotify 불필요하므로 쓰기 가능


핵심 함수

1. do_mprotect_pkey() — 시스템 호출 진입점

// mm/mprotect.c:801-946
static int do_mprotect_pkey(unsigned long start, size_t len,
                            unsigned long prot, int pkey)

* 역할: mprotect()/pkey_mprotect() 시스템 호출의 공통 처리 함수

* 분기 로직:

1. PROT_GROWSDOWN|PROT_GROWSUP 동시 설정 시 EINVAL

2. mmap_write_lock_killable()로 mmap_lock 획득

3. pkey가 -1이 아니면 mm_pkey_is_allocated()로 할당 검증

4. vma_find()로 대상 VMA 탐색, 없으면 ENOMEM

5. PROT_GROWSDOWN/PROT_GROWSUP 처리로 start/end 범위 확장

6. for_each_vma_range()로 각 VMA에 대해:

- rier(READ_IMPLIES_EXEC) 처리

- calc_vm_prot_bits()로 newflags 계산

- VM_ACCESS_FLAGS 검증 → 권한 없는 플래그 요청 시 EACCES

- map_deny_write_exec()로 MDWE 위반 검사

- security_file_mprotect()로 LSM 검증

- vma->vm_ops->mprotect() 콜백 호출 (파일 시스템별)

- mprotect_fixup()로 실제 VMA 수정

7. tlb_finish_mmu()로 TLB 플러시

2. mprotect_fixup() — VMA 보호 수정

// mm/mprotect.c:695-796
int mprotect_fixup(struct vma_iterator *vmi, struct mmu_gather *tlb,
                   struct vm_area_struct *vma, struct vm_area_struct **pprev,
                   unsigned long start, unsigned long end, vm_flags_t newflags)

* 역할: 단일 VMA의 보호 속성을 실제로 변경

* 분기 로직:

1. vma_is_sealed(vma)EPERM (봉인된 VMA 거부)

2. newflags == oldflags → 즉시 반환

3. VM_PFNMAP|VM_MIXEDMAP이고 VM_ACCESS_FLAGS 제거 시 walk_page_range()로 PROT_NONE 사전 검증

4. VM_WRITE 추가 시 오버커밋 검증 (may_expand_vm, security_vm_enough_memory_mm)

5. VM_ACCOUNT 해제 시 vm_unacct_memory()

6. vma_modify_flags()로 VMA 병합/분할 처리

7. vma_start_write()vm_flags_reset_once()vma_set_page_prot()

8. change_protection()로 페이지 테이블 업데이트

9. VM_LOCKED → writable 전환 시 populate_vma_page_range()로 COW 트리거

3. change_protection() — 페이지 테이블 보호 변경

// mm/mprotect.c:633-662
long change_protection(struct mmu_gather *tlb,
                       struct vm_area_struct *vma, unsigned long start,
                       unsigned long end, unsigned long cp_flags)

* 역할: 지정된 주소 범위의 페이지 테이블 보호 속성을 변경

* 분기 로직:

1. NUMA 밸런싱이면 newprot = PAGE_NONE (PROT_NONE으로 비활성화)

2. Hugetlb 페이지면 hugetlb_change_protection() 분기

3. 일반 페이지면 change_protection_range() 호출

4. change_protection_range() → change_pmd_range() → change_pte_range()

페이지 테이블 5단계(PGD→P4D→PUD→PMD→PTE)를 순회하며 보호를 변경하는 재귀 구조이다.

change_protection_range()     ← PGD 레벨 순회
  └─ change_p4d_range()       ← P4D 레벨 순회
       └─ change_pud_range()  ← PUD 레벨 순회 (huge pud 처리)
            └─ change_pmd_range() ← PMD 레벨 순회 (huge pmd 처리)
                 └─ change_pte_range() ← 실제 PTE 수정

5. change_pte_range() — PTE 레벨 처리

// mm/mprotect.c:214-386
static long change_pte_range(struct mmu_gather *tlb,
        struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr,
        unsigned long end, pgprot_t newprot, unsigned long cp_flags)

* 역할: PTE 하나(또는 배치)의 보호 속성을 변경

* 3가지 PTE 상태 처리:

1. pte_present(oldpte): 존재하는 페이지 → pte_modify()로 보호 변경, uffd_wp/uffd_wp_resolve 처리

2. pte_none(oldpte): 빈 PTE → uffd_wp일 때 PTE_MARKER_UFFD_WP 마커 설치

3. 그 외 (스왑/마이그레이션/디바이스): softleaf_t로 타입 분류 후 마이그레이션 엔트리, 디바이스 프라이빗 엔트리, 마커 엔트리 각각 처리

* 배치 최적화: mprotect_folio_pte_batch()로 large folio의 연속 PTE를 한번에 처리

6. pkey_alloc / pkey_free / pkey_mprotect — 메모리 보호 키

// mm/mprotect.c:956-1005
SYSCALL_DEFINE4(pkey_mprotect, unsigned long, start, size_t, len,
                unsigned long, prot, int, pkey)
SYSCALL_DEFINE2(pkey_alloc, unsigned long, flags, unsigned long, init_val)
SYSCALL_DEFINE1(pkey_free, int, pkey)

* pkey_mprotect: pkey를 지정하여 mprotect 적용 (x86 Memory Protection Keys)

* pkey_alloc: 새 보호 키 할당 후 arch_set_user_pkey_access()로 권한 설정

* pkey_free: 보호 키 해제


호출 흐름

sys_mprotect(start, len, prot)
  └─ do_mprotect_pkey(start, len, prot, pkey=-1)
       ├─ mmap_write_lock_killable()          ← mmap_lock 쓰기 잠금
       ├─ vma_find()                          ← 대상 VMA 탐색
       ├─ [for_each_vma_range] 각 VMA 반복:
       │    ├─ calc_vm_prot_bits()            ← PROT_* → VM_* 변환
       │    ├─ map_deny_write_exec()          ← MDWE 검증
       │    ├─ security_file_mprotect()       ← LSM 보안 검증
       │    ├─ vma->vm_ops->mprotect()        ← 파일 시스템 콜백
       │    └─ mprotect_fixup()               ← VMA 수정
       │         ├─ vma_is_sealed()           ← 봉인 검사
       │         ├─ vma_modify_flags()        ← VMA 병합/분할
       │         ├─ vm_flags_reset_once()     ← 플래그 업데이트
       │         ├─ vma_set_page_prot()       ← vm_page_prot 갱신
       │         └─ change_protection()       ← PTE 보호 변경
       │              ├─ [Hugetlb] hugetlb_change_protection()
       │              └─ [일반] change_protection_range()
       │                   └─ change_p4d_range() → change_pud_range()
       │                        → change_pmd_range()
       │                             └─ change_pte_range()
       │                                  ├─ pte_present: pte_modify + TLB flush
       │                                  ├─ pte_none: uffd-wp 마커 설치
       │                                  └─ swap/dev: softleaf 처리
       └─ tlb_finish_mmu()                    ← TLB 플러시 완료

조건별 비교

시스템 호출별 비교

항목mprotect()pkey_mprotect()pkey_alloc()pkey_free()
시스템 호출 번호SYSCALL_DEFINE3SYSCALL_DEFINE4SYSCALL_DEFINE2SYSCALL_DEFINE1
pkey 매개변수-1 (없음)사용자 지정반환값해제 대상
메모리 보호 키불가가능할당만해제만
CONFIG_ARCH_HAS_PKEYS불필요필요필요필요
내부 처리do_mprotect_pkey(start,len,prot,-1)do_mprotect_pkey(start,len,prot,pkey)mm_pkey_alloc + arch_set_user_pkey_accessmm_pkey_free

PTE 보호 변경 조건별 분기

조건처리 방식코드 위치
pte_present && prot_numa && pte_protnone건너뜀 (이미 PROT_NONE)mprotect.c:248-249
pte_present && prot_numa && !folio_can_map_prot_numa배치 건너뜀mprotect.c:259-267
pte_present && !prot_numapte_modify() + writable 시도mprotect.c:269-299
pte_none && uffd_wp && userfaultfd_wp_use_markersPTE_MARKER_UFFD_WP 설치mprotect.c:308-318
pte_none && !uffd_wp건너뜀mprotect.c:305-306
swap/migration entrysoftleaf 타입별 처리mprotect.c:320-379
uffd_wp_resolve && pte_none markerpte_clear()mprotect.c:362-364

COW/쓰기 관련 분기

VMA 타입쓰기 가능 조건코드 위치
MAP_PRIVATE (비공유)PageAnon && PageAnonExclusivemprotect.c:61-77
MAP_SHARED (공유)pte_dirty (이미 쓰기됨)mprotect.c:79-95
MAP_PRIVATE + VM_LOCKED → writablepopulate_vma_page_range()로 COW 트리거mprotect.c:783-786
writenotify 필요한 공유 매핑pte_clean으로 쓰기 불가 유지mprotect.c:88-94

핵심 흐름: PTE 쓰기 변경 최적화

Linux 7.0의 mprotect는 large folio에서 연속된 PTE를 배치(batching)로 처리하여 성능을 향상시킨다.

change_pte_range()
  ├─ mprotect_folio_pte_batch()        ← large folio에서 처리할 PTE 수 계산
  │    └─ folio_pte_batch_flags()       ← 연속 PTE 스캔
  ├─ modify_prot_start_ptes()          ← 배치 시작 (read-modify-write 트랜잭션)
  ├─ pte_modify()                      ← 새 보호 속성 적용
  ├─ [MM_CP_TRY_CHANGE_WRITABLE]
  │    └─ set_write_prot_commit_flush_ptes()  ← 쓰기 가능 여부 결정
  │         ├─ [공유] can_change_shared_pte_writable()
  │         └─ [비공유] commit_anon_folio_batch()
  │              └─ page_anon_exclusive_sub_batch()  ← 페이지별 exclusive 검증
  └─ modify_prot_commit_ptes()         ← 배치 커밋 + TLB flush

관련 문서

  • 메모리 관리 개요
  • VMA / mmap
  • Huge Pages / THP
  • NUMA
  • Userfaultfd
  • Sealing (mseal)
  • mremap
  • Page Table Walk