mseal()은 Linux 커널의 시스템 호출로, 사용자 공간에서 가상 메모리 영역(VMA)의 메타데이터를 변경할 수 없도록 "봉인(sealing)"하는 보안 기능을 제공합니다. 메모리 봉인은 RWX(Read/Write/Execute) 비트 외에 매핑 자체를 보호하여, 손상된 포인터가 메모리 관리 시스템을 악용하는 것을 방지합니다. 이는 XNU 커널의 VM_FLAGS_PERMANENT이나 OpenBSD의 mimmutable과 유사한 기능입니다.
Linux 7.0에서 mseal()은 64비트 아키텍처에서만 지원되며(32비트는 VM_SEALED가 VM_NONE으로 정의), CONFIG_64BIT이 활성화된 시스템에서만 동작합니다. 봉인된 매핑은 프로세스가 종료되거나 exec 시스템 호출이 발생할 때까지 해제될 수 없으며, 한번 봉인된 메모리는 munseal 기능이 존재하지 않습니다.
mm/mseal.c # 핵심 구현 (191줄)
mm/vma.h # vma_is_sealed() 헬퍼
mm/vma.c # munmap 경로에서 봉인 검사
mm/mremap.c # mremap 차단 검사
mm/mprotect.c # mprotect 차단 검사
mm/madvise.c # madvise 차단 검사
fs/binfmt_elf.c # ELF 로더에서 page 0 자동 봉인
include/linux/mm.h # VM_SEALED 플래그 정의
init/Kconfig # CONFIG_MSEAL_SYSTEM_MAPPINGS
Documentation/userspace-api/mseal.rst # 공식 문서
# 1. mseal 시스템 호출 지원 여부 확인
grep -r "CONFIG_64BIT" /boot/config-$(uname -r)
# 2. do_mseal 심볼 확인
grep -w "do_mseal" /proc/kallsyms 2>/dev/null || echo "do_mseal 심볼 미노출"
# 3. 봉인 관련 커널 모듈/설정 확인
zgrep "MSEAL" /proc/config.gz 2>/dev/null || grep "MSEAL" /boot/config-$(uname -r) 2>/dev/null
# 4. 시스템 매핑 봉인 활성화 확인
cat /proc/sys/vm/mmap_min_addr # 최소 주소 확인
grep -n "vdso\|vvar" /proc/self/maps # 시스템 매핑 확인
# 5. mseal 관련 커널 로그 확인
dmesg | grep -i "seal\|mseal" 2>/dev/null
# 6. seccomp에서 mseal 호출 허용 여부 확인
grep -i seccomp /proc/self/status
# 7. VMA 개수 제한 확인 (봉인 시 VMA 분할 가능성)
cat /proc/sys/vm/max_map_count
# 8. 특정 프로세스의 봉인된 매핑 확인 (VmFlags에 'sl' 표시)
grep -A 5 "VmFlags:.*sl" /proc/<pid>/smaps
mseal()은 현재 작업의 주소 공간(struct mm_struct)과 그 안의 VMA(struct vm_area_struct)를 함께 다룹니다. mm_struct는 잠금과 범위 검색의 기준이 되고, vm_area_struct는 vm_start, vm_end, vm_flags를 기준으로 봉인 여부를 바꿉니다.
/* mm/mseal.c:55-65 */
static int mseal_apply(struct mm_struct *mm,
unsigned long start, unsigned long end)
{
struct vm_area_struct *vma, *prev;
VMA_ITERATOR(vmi, mm, start);
vma = vma_iter_load(&vmi);
prev = vma_prev(&vmi);
if (start > vma->vm_start)
prev = vma;
struct mm_struct: 현재 프로세스의 주소 공간 전체를 대표합니다. mmap_write_lock_killable(mm)와 range_contains_unmapped(mm, ...)의 기준입니다.struct vm_area_struct: 봉인 대상 구간입니다. vma_modify_flags()가 이 구조체의 vm_flags를 갱신합니다.VM_SEALED는 vm_area_struct.vm_flags에 설정되는 비트 플래그로, 해당 VMA가 봉인되었음을 나타냅니다.
/* include/linux/mm.h:500-506 */
#ifdef CONFIG_64BIT
#define VM_SEALED INIT_VM_FLAG(SEALED) /* 64비트에서만 유효 */
#else
#define VM_SEALED VM_NONE /* 32비트에서는 비활성 */
#endif
/* include/linux/mm.h:536-540 - 시스템 매핑 봉인 */
#ifdef CONFIG_MSEAL_SYSTEM_MAPPINGS
#define VM_SEALED_SYSMAP VM_SEALED /* 시스템 매핑 자동 봉인 */
#else
#define VM_SEALED_SYSMAP VM_NONE /* 시스템 매핑 봉인 비활성 */
#endif
/proc//smaps 의 VmFlags에는 sl로 표시됩니다.VMA가 봉인되었는지 확인하는 인라인 함수입니다.
/* mm/vma.h:661-671 */
#ifdef CONFIG_64BIT
static inline bool vma_is_sealed(struct vm_area_struct *vma)
{
return (vma->vm_flags & VM_SEALED); /* VM_SEALED 비트 검사 */
}
#else
static inline bool vma_is_sealed(struct vm_area_struct *vma)
{
return false; /* 32비트에서는 항상 false */
}
#endif
mseal() 호출 시 시작/끝 주소 사이에 매핑되지 않은 영역(VMA hole)이 있는지 확인합니다.
/* mm/mseal.c:38-53 */
static bool range_contains_unmapped(struct mm_struct *mm,
unsigned long start, unsigned long end)
{
struct vm_area_struct *vma;
unsigned long prev_end = start;
VMA_ITERATOR(vmi, current->mm, start);
for_each_vma_range(vmi, vma, end) {
if (vma->vm_start > prev_end)
return true; /* 이전 끝과 현재 시작 사이에 홀 존재 */
prev_end = vma->vm_end;
}
return prev_end < end; /* 마지막 VMA가 end보다 먼저 끝나는 경우 */
}
실제로 VMA에 VM_SEALED 플래그를 설정하는 함수입니다. VMA 분할/병합이 필요할 수 있습니다.
/* mm/mseal.c:55-85 */
static int mseal_apply(struct mm_struct *mm,
unsigned long start, unsigned long end)
{
struct vm_area_struct *vma, *prev;
VMA_ITERATOR(vmi, mm, start);
vma = vma_iter_load(&vmi);
prev = vma_prev(&vmi);
if (start > vma->vm_start)
prev = vma;
for_each_vma_range(vmi, vma, end) {
const unsigned long curr_start = MAX(vma->vm_start, start);
const unsigned long curr_end = MIN(vma->vm_end, end);
if (!(vma->vm_flags & VM_SEALED)) {
vm_flags_t vm_flags = vma->vm_flags | VM_SEALED;
/* VMA 수정 (분할/병합 필요 시) */
vma = vma_modify_flags(&vmi, prev, vma, curr_start,
curr_end, &vm_flags);
if (IS_ERR(vma))
return PTR_ERR(vma);
vm_flags_set(vma, VM_SEALED);
}
prev = vma;
}
return 0;
}
/* mm/mseal.c:139-185 */
int do_mseal(unsigned long start, size_t len_in, unsigned long flags)
mseal() 시스템 호출의 메인 로직. 입력 검증 후 봉인 적용 - flags != 0 → -EINVAL (예약된 플래그)
- start가 페이지 정렬되지 않음 → -EINVAL
- len_in이 0으로 라운딩됨 → -EINVAL
- end < start (오버플로우) → -EINVAL
- end == start → 성공 (아무 작업 없음)
- range_contains_unmapped()가 true → -ENOMEM
- mseal_apply() 실패 → 해당 에러 코드 반환
/* mm/mseal.c:187-191 */
SYSCALL_DEFINE3(mseal, unsigned long, start, size_t, len, unsigned long, flags)
{
return do_mseal(start, len, flags);
}
int mseal(void *addr, size_t len, unsigned long flags)/* mm/madvise.c:1297-1331 */
static bool can_madvise_modify(struct madvise_behavior *madv_behavior)
{
struct vm_area_struct *vma = madv_behavior->vma;
/* 봉인되지 않은 VMA면 허용 */
if (!vma_is_sealed(vma))
return true;
/* 봉인된 VMA라도 discard가 아니면 허용 */
if (!is_discard(madv_behavior->behavior))
return true;
/* 익명 read-only SEALED 매핑의 일부 discard는 허용 */
if (!vma_is_anonymous(vma))
return true;
/* 쓰기 가능한 매핑이면 허용 */
if ((vma->vm_flags & VM_WRITE) &&
arch_vma_access_permitted(vma, /* 쓰기= */ true,
/* 실행= */ false, /* 외부= */ false))
return true;
/* 그 외에는 허용하지 않음 */
return false;
}
madvise() 호출이 허용되는지 판단- VMA가 봉인되지 않음 → 허용
- discard 연산이 아님 → 허용
- 파일 기반 매핑 (shared/private) → 허용
- 쓰기 가능 매핑 → 허용
- 그 외 (익명 읽기 전용 봉인 매핑) → 차단
/* mm/madvise.c:1273-1284 */
static bool is_discard(int behavior)
{
switch (behavior) {
case MADV_FREE:
case MADV_DONTNEED:
case MADV_DONTNEED_LOCKED:
case MADV_REMOVE:
case MADV_DONTFORK:
case MADV_WIPEONFORK:
case MADV_GUARD_INSTALL:
return true;
}
return false;
}
madvise()가 데이터 폐기 계열인지 먼저 가려낸 뒤, 봉인된 익명 읽기 전용 VMA에서만 제한을 걸어둡니다./* mm/vma.h:662-664 */
static inline bool vma_is_sealed(struct vm_area_struct *vma)
{
return (vma->vm_flags & VM_SEALED);
}
vm_flags에서 VM_SEALED 비트 검사mremap, mprotect, munmap, madvise 등에서 호출/* mm/mseal.c:38-53 */
static bool range_contains_unmapped(struct mm_struct *mm,
unsigned long start, unsigned long end)
true, 없으면 false사용자 공간
│
▼
mseal(addr, len, flags) ← 시스템 호출
│
▼
SYSCALL_DEFINE3(mseal) ← mm/mseal.c:187
│
▼
do_mseal(start, len, flags) ← mm/mseal.c:139
│
├─► flags 검증 ← flags != 0 → -EINVAL
├─► untagged_addr(start) ← 태그 제거 (MTE 등)
├─► PAGE_ALIGNED(start) 검증 ← 페이지 정렬 확인
├─► len = PAGE_ALIGN(len_in) ← 길이 정렬
├─► end = start + len ← 오버플로우 검사
│
├─► mmap_write_lock_killable(mm) ← 쓰기 잠금 획득
│
├─► range_contains_unmapped(mm, start, end) ← VMA 홀 검사
│ │
│ └─► for_each_vma_range() → 홀 발견 시 -ENOMEM
│
├─► mseal_apply(mm, start, end) ← VM_SEALED 플래그 설정
│ │
│ └─► for_each_vma_range()
│ ├─► vma_modify_flags() ← VMA 분할/병합
│ └─► vm_flags_set(vma, VM_SEALED)
│
└─► mmap_write_unlock(mm) ← 쓰기 잠금 해제
봉인된 VMA에서 호출 시
│
├─► munmap() → mm/vma.c:1403,1423 → -EPERM
├─► mremap() → mm/mremap.c:1666 → -EPERM
├─► mprotect() → mm/mprotect.c:706 → -EPERM
└─► madvise() → mm/madvise.c:1302 → 허용/차단 분기
| 시스템 호출 | 허용 여부 | 검사 위치 | 비고 |
|---|---|---|---|
| `munmap()` | 차단 (-EPERM) | mm/vma.c:1403 | 봉인된 VMA 분할 시도 시 |
| `mremap()` | 차단 (-EPERM) | mm/mremap.c:1666 | 이동/확장/축소 모두 차단 |
| `mprotect()` | 차단 (-EPERM) | mm/mprotect.c:706 | RWX 보호 비트 변경 차단 |
| `pkey_mprotect()` | 차단 (-EPERM) | mm/mprotect.c:706 | pkey 포함 mprotect |
| `madvise(discard 계열)` | 조건부 차단 | mm/madvise.c:1274,1302 | `MADV_FREE`, `MADV_DONTNEED`, `MADV_DONTNEED_LOCKED`, `MADV_REMOVE`, `MADV_DONTFORK`, `MADV_WIPEONFORK`, `MADV_GUARD_INSTALL` |
| `madvise(비-discard)` | 허용 | mm/madvise.c:1306 | `is_discard()`가 false이면 허용 |
| `mmap(MAP_FIXED)` | 차단 (간접) | mm/vma.c:1403 | 기존 매핑 교체 불가 |
| 조건 | 결과 | 비고 |
|---|---|---|
| `flags != 0` | -EINVAL | 예약된 플래그 |
| 시작 주소 미정렬 | -EINVAL | 페이지 정렬 필요 |
| 범위 오버플로우 | -EINVAL | start + len > ULONG_MAX |
| 빈 범위 (len=0) | 성공 (0) | 아무 작업 없음 |
| VMA 홀 존재 | -ENOMEM | 시작/끝 사이 매핑 없음 |
| 32비트 아키텍처 | -EPERM | mseal 미지원 |
| 이미 봉인된 VMA | 성공 (0) | 멱등성 (idempotent) |
| 정상적인 봉인 | 성공 (0) | VM_SEALED 플래그 설정 |
| 설정 | 동작 | 지원 아키텍처 |
|---|---|---|
| `CONFIG_MSEAL_SYSTEM_MAPPINGS=y` | vdso, vvar 등 시스템 매핑 자동 봉인 | x86-64, arm64, loongarch, s390 |
| `CONFIG_MSEAL_SYSTEM_MAPPINGS=n` | 시스템 매핑 봉인 비활성 | 모든 아키텍처 |
| `CONFIG_ARCH_SUPPORTS_MSEAL_SYSTEM_MAPPINGS` | 아키텍처 수준 지원 | x86-64, arm64, loongarch, s390 |
/* fs/binfmt_elf.c:1358 - SVr4 호환 page 0 자동 봉인 */
if (current->personality & MMAP_PAGE_ZERO) {
error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
retval = do_mseal(0, PAGE_SIZE, 0); /* page 0 봉인 */
}
봉인된 읽기 전용 매핑에도 쓰기가 가능한 경로가 있습니다 (보안 고려사항):
| 경로 | 메커니즘 | 차단 방법 |
|---|---|---|
| `/proc/self/mem` | FOLL_FORCE로 쓰기 | SELinux/SMACK |
| `ptrace` | PTRACE_POKETEXT | seccomp |
| `userfaultfd` | 페이지 폴트 처리 | seccomp |