# mprotect 🛡️
관련 소스:mm/mprotect.c,include/linux/mman.h,include/linux/mm.h
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
# 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
// 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 (핵심 필드)
// 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 연동)
// 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으로 페이지를 비활성화할 때 사용
// 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 불필요하므로 쓰기 가능
// 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 플러시
// 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 트리거
// 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() 호출
페이지 테이블 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 수정
// 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를 한번에 처리
// 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_DEFINE3 | SYSCALL_DEFINE4 | SYSCALL_DEFINE2 | SYSCALL_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_access | mm_pkey_free |
| 조건 | 처리 방식 | 코드 위치 |
|---|---|---|
| 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_numa | pte_modify() + writable 시도 | mprotect.c:269-299 |
| pte_none && uffd_wp && userfaultfd_wp_use_markers | PTE_MARKER_UFFD_WP 설치 | mprotect.c:308-318 |
| pte_none && !uffd_wp | 건너뜀 | mprotect.c:305-306 |
| swap/migration entry | softleaf 타입별 처리 | mprotect.c:320-379 |
| uffd_wp_resolve && pte_none marker | pte_clear() | mprotect.c:362-364 |
| VMA 타입 | 쓰기 가능 조건 | 코드 위치 |
|---|---|---|
| MAP_PRIVATE (비공유) | PageAnon && PageAnonExclusive | mprotect.c:61-77 |
| MAP_SHARED (공유) | pte_dirty (이미 쓰기됨) | mprotect.c:79-95 |
| MAP_PRIVATE + VM_LOCKED → writable | populate_vma_page_range()로 COW 트리거 | mprotect.c:783-786 |
| writenotify 필요한 공유 매핑 | pte_clean으로 쓰기 불가 유지 | mprotect.c:88-94 |
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