Ryotta's Linux 7.0 MM

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

Secret Memory — `memfd_secret(2)` 시스템 호출

개요

Secret Memory는 Linux 커널에서 _userspace만 접근 가능한 메모리 영역_을 생성하는 메커니즘이다. memfd_secret(2) 시스템 호출을 통해 생성된 파일 디스크립터를 mmap하면, 해당 메모리는 커널의 직접 매핑(direct map)에서 제외된다. 즉, 커널이 페이지 테이블에서 해당 페이지를 제거하여 자체적으로도 읽을 수 없게 된다. 이는 비밀키 저장소, 암호화 키 관리,TEE(Trusted Execution Environment) 런타임과 같은 보안이 중요한 사용 사례에 활용된다.

핵심 원리는 간단하다: 페이지를 할당한 뒤 set_direct_map_invalid_noflush()를 호출하여 커널의 linear map에서 해당 페이지를 unmap하고, TLB 플러시를 통해 실제 매핑을 무효화한다. 이후 사용자 공간만이 페이지 테이블 엔트리를 통해 해당 페이지에 접근할 수 있다. GUP(Get User Pages) 경로에서도 secretmem 페이지는 명시적으로 거부되어 커널이 우회할 수 없도록 차단된다.

소스 파일 경로:
mm/secretmem.c                           ← 메인 구현 (270줄)
include/linux/secretmem.h                ← 헤더 (36줄)
mm/gup.c                                 ← GUP-fast secretmem 거부 로직
mm/mlock.c                               ← VM_LOCKED 플래그 처리
arch/x86/mm/pat/set_memory.c             ← set_direct_map_invalid_noflush 구현
fs/anon_inodes.c                         ← anon_inode_make_secure_inode

빠른 점검 명령

# 1. secretmem 활성화 여부 확인 (커널 빌드 옵션)
cat /boot/config-$(uname -r) | grep SECRETMEM

# 2. memfd_secret 시스템 호출 존재 여부 확인
grep -c memfd_secret /usr/include/asm/unistd_64.h

# 3. 현재 secretmem 사용자 수 (활성 파일 디스크립터)
sudo cat /proc/sys/kernel/secretmem_users 2>/dev/null || echo "sysctl 미지원"

# 4. 커널 모듈/파라미터 확인
cat /sys/module/secretmem/parameters/enable 2>/dev/null || echo "모듈 미로드"

# 5. /proc/meminfo에서 secretmem 관련 확인
grep -i secret /proc/meminfo 2>/dev/null || echo "secretmem 전용 항목 없음"

# 6. 실제 테스트 프로그램 컴파일 및 실행
cat > /tmp/test_secretmem.c << 'EOF'
#include <stdio.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = syscall(SYS_memfd_secret, O_CLOEXEC);
    if (fd < 0) { perror("memfd_secret"); return 1; }
    void *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) { perror("mmap"); return 1; }
    /* 사용자 공간에서만 접근 가능 — 커널 직접 매핑에서 제외됨 */
    printf("secretmem 페이지 할당 성공: %p\n", p);
    munmap(p, 4096);
    close(fd);
    return 0;
}
EOF
gcc /tmp/test_secretmem.c -o /tmp/test_secretmem && /tmp/test_secretmem

# 7. ARM64 등 아키텍처별 direct map 제한 확인
zgrep CONFIG_ARCH_HAS_SET_DIRECT_MAP /proc/config.gz 2>/dev/null || echo "config 미지원"

핵심 자료구조

`secretmem_vm_ops` — VMA 연산 테이블

// mm/secretmem.c:111-113
static const struct vm_operations_struct secretmem_vm_ops = {
    .fault = secretmem_fault,  /* 페이지 폴트 발생 시 호출되는 핸들러 */
};
  • 역할: secretmem VMA에 페이지 폴트가 발생하면 secretmem_fault()를 호출한다.
  • 식별자: vma->vm_ops == &secretmem_vm_ops로 secretmem VMA 여부를 판단한다.
  • `secretmem_fops` — 파일 연산 테이블

    // mm/secretmem.c:141-144
    static const struct file_operations secretmem_fops = {
        .release       = secretmem_release,       /* fd close 시 사용자 수 감소 */
        .mmap_prepare  = secretmem_mmap_prepare,  /* mmap 준비 단계에서 VMA 설정 */
    };
  • 역할: 파일 디스크립터의 mmap과 close 동작을 정의한다.
  • mmap_prepare은 Linux 7.0에서 도입된 새로운 mmap 경로로, VMA 생성 전에 검증과 설정을 수행한다.
  • `secretmem_aops` — 주소 공간 연산 테이블

    // mm/secretmem.c:158-162
    const struct address_space_operations secretmem_aops = {
        .dirty_folio    = noop_dirty_folio,          /* 더티 트래킹 불필요 */
        .free_folio     = secretmem_free_folio,      /* 해제 시 direct map 복원 + 제로화 */
        .migrate_folio  = secretmem_migrate_folio,   /* 마이그레이션 거부 (-EBUSY) */
    };
  • 역할: secretmem 페이지의 lifecycle을 관리한다.
  • migrate_folio-EBUSY를 반환하는 이유: secretmem 페이지는 커널이 직접 접근할 수 없으므로 마이그레이션이 불가능하다.
  • free_folio에서 set_direct_map_default_noflush()로 direct map을 복원한 뒤 folio_zero_segment()로 내용을 제로화한다.
  • `secretmem_fs` — Pseudo 파일 시스템

    // mm/secretmem.c:253-257
    static struct file_system_type secretmem_fs = {
        .name             = "secretmem",
        .init_fs_context  = secretmem_init_fs_context,
        .kill_sb          = kill_anon_super,
    };
  • 역할: memfd_secret이 사용하는 가상 파일 시스템이다.
  • SB_I_NOEXEC | SB_I_NODEV 플래그로 실행/장치 접근을 차단한다.
  • 실제 디스크에 연결되지 않는 pseudo-fs 기반으로 동작한다.
  • `secretmem_enable` — 모듈 파라미터

    // mm/secretmem.c:38-41
    static bool secretmem_enable __ro_after_init = 1;
    module_param_named(enable, secretmem_enable, bool, 0400);
    MODULE_PARM_DESC(secretmem_enable,
                     "Enable secretmem and memfd_secret(2) system call");
  • 역할: 부팅 시 secretmem 기능의 활성화/비활성화를 제어한다.
  • __ro_after_init으로 초기화 후 읽기 전용이 되어 런타임 변경을 방지한다.
  • 커널 빌드 시 CONFIG_SECRETMEM=n으로 완전히 비활성화할 수도 있다.
  • `secretmem_users` — 활성 사용자 카운터

    // mm/secretmem.c:43
    static atomic_t secretmem_users;
  • 역할: 현재 열려 있는 secretmem 파일 디스크립터 수를 추적한다.
  • secretmem_active()로 커널 전체에서 secretmem이 사용 중인지 확인할 수 있다.
  • memfd_secret 시스템 호출에서 atomic_read(&secretmem_users) < 0 검사로 오버플로우를 방지한다.

  • 핵심 함수

    1. `secretmem_fault()` — 페이지 폴트 핸들러

    // mm/secretmem.c:50-109
    static vm_fault_t secretmem_fault(struct vm_fault *vmf)

    역할: secretmem VMA에서 페이지 폴트가 발생했을 때, 새 folio를 할당하고 커널 direct map에서 제거한다.

    // mm/secretmem.c:61-100
    if (((loff_t)vmf->pgoff << PAGE_SHIFT) >= i_size_read(inode))
        return vmf_error(-EINVAL);
    
    filemap_invalidate_lock_shared(mapping);
    
    retry:
        folio = filemap_lock_folio(mapping, offset);
        if (IS_ERR(folio)) {
            folio = folio_alloc(gfp | __GFP_ZERO, 0);
            if (!folio) {
                ret = VM_FAULT_OOM;
                goto out;
            }
    
            err = set_direct_map_invalid_noflush(folio_page(folio, 0));
            if (err) {
                folio_put(folio);
                ret = vmf_error(err);
                goto out;
            }
    
            __folio_mark_uptodate(folio);
            err = filemap_add_folio(mapping, folio, offset, gfp);
            if (unlikely(err)) {
                /* 큰 페이지 분할이 필요했다면, 무효화 표시 시점에 이미 수행됨 */
                set_direct_map_default_noflush(folio_page(folio, 0));
                folio_put(folio);
                if (err == -EEXIST)
                    goto retry;
    
                ret = vmf_error(err);
                goto out;
            }
    
            addr = (unsigned long)folio_address(folio);
            flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
        }

    흐름:

    1. 파일 범위 초과 검사 (i_size_read)

    2. filemap_lock_folio()로 기존 folio 탐색

    3. 없으면 folio_alloc(gfp | __GFP_ZERO, 0)로 새 folio 할당

    4. set_direct_map_invalid_noflush()로 커널 직접 매핑 제거

    5. filemap_add_folio()로 매핑에 추가

    6. flush_tlb_kernel_range()로 TLB 무효화

    7. 기존 folio가 있으면 그대로 반환 (VM_FAULT_LOCKED)

    반복 로직: filemap_add_folio()-EEXIST를 반환하면 goto retry로 재시도한다.

    2. `secretmem_mmap_prepare()` — mmap 사전 설정

    // mm/secretmem.c:121-134
    static int secretmem_mmap_prepare(struct vm_area_desc *desc)

    역할: mmap 시 VMA 플래그를 설정하고 secretmem 전용 vm_ops를 연결한다.

    // mm/secretmem.c:123-133
    const unsigned long len = vma_desc_size(desc);
    
    if (!vma_desc_test_flags(desc, VMA_SHARED_BIT, VMA_MAYSHARE_BIT))
        return -EINVAL;
    
    vma_desc_set_flags(desc, VMA_LOCKED_BIT, VMA_DONTDUMP_BIT);
    if (!mlock_future_ok(desc->mm, /* VMA 고정 여부 = */ true, len))
        return -EAGAIN;
    desc->vm_ops = &secretmem_vm_ops;
    
    return 0;

    흐름:

    1. VMA_SHARED_BIT | VMA_MAYSHARE_BIT 확인 → 공유 매핑만 허용

    2. VMA_LOCKED_BIT | VMA_DONTDUMP_BIT 설정 → VMA 고정 + 코어 덤프 제외

    3. mlock_future_ok() 확인 → mlock 한도 초과 시 -EAGAIN

    4. desc->vm_ops = &secretmem_vm_ops 연결

    제약 조건: secretmem은 반드시 shared 매핑으로만 사용할 수 있다.

    3. `secretmem_file_create()` — 파일 생성

    // mm/secretmem.c:190-222
    static struct file *secretmem_file_create(unsigned long flags)

    역할: memfd_secret 시스템 호출의 핵심으로, anonymous inode 기반 파일을 생성한다.

    // mm/secretmem.c:196-215
    inode = anon_inode_make_secure_inode(secretmem_mnt->mnt_sb, anon_name, NULL);
    if (IS_ERR(inode))
        return ERR_CAST(inode);
    
    file = alloc_file_pseudo(inode, secretmem_mnt, "secretmem",
                             O_RDWR | O_LARGEFILE, &secretmem_fops);
    if (IS_ERR(file))
        goto err_free_inode;
    
    mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER);
    mapping_set_unevictable(inode->i_mapping);
    
    inode->i_op = &secretmem_iops;
    inode->i_mapping->a_ops = &secretmem_aops;
    
    /* zero size인 일반 파일처럼 보이게 처리 */
    inode->i_mode |= S_IFREG;
    inode->i_size = 0;
    
    atomic_inc(&secretmem_users);

    흐름:

    1. anon_inode_make_secure_inode()로 보안 컨텍스트가 적용된 anonymous inode 생성

    2. alloc_file_pseudo()로 pseudo 파일 할당

    3. mapping_set_gfp_mask(GFP_HIGHUSER) → 고유 메모리에서만 할당

    4. mapping_set_unevictable() → 회수 불가능하도록 설정

    5. inode에 secretmem_iops, secretmem_aops 연결

    6. atomic_inc(&secretmem_users)로 사용자 수 증가

    4. `memfd_secret()` — 시스템 호출 진입점

    // mm/secretmem.c:224-238
    SYSCALL_DEFINE1(memfd_secret, unsigned int, flags)

    역할: 사용자 공간의 memfd_secret(2) 시스템 호출을 처리한다.

    // mm/secretmem.c:226-237
    /* 로컬 flags가 전역 fcntl.h와 충돌하지 않는지 확인 */
    BUILD_BUG_ON(SECRETMEM_FLAGS_MASK & O_CLOEXEC);
    
    if (!secretmem_enable || !can_set_direct_map())
        return -ENOSYS;
    
    if (flags & ~(SECRETMEM_FLAGS_MASK | O_CLOEXEC))
        return -EINVAL;
    if (atomic_read(&secretmem_users) < 0)
        return -ENFILE;
    
    return FD_ADD(flags & O_CLOEXEC, secretmem_file_create(flags));

    흐름:

    1. secretmem_enablecan_set_direct_map() 확인 → 미지원 시 -ENOSYS

    2. 플래그 유효성 검증 (mode/flag 마스크 + O_CLOEXEC)

    3. secretmem_user 수 오버플로우 검사

    4. secretmem_file_create() 호출 후 FD_ADD()로 fd 반환

    반환: 성공 시 파일 디스크립터, 실패 시 errno.

    5. `secretmem_free_folio()` — folio 해제

    // mm/secretmem.c:152-156
    static void secretmem_free_folio(struct folio *folio)

    역할: secretmem folio가 해제될 때 direct map을 복원하고 내용을 제로화한다.

    흐름:

    1. set_direct_map_default_noflush() → 커널 직접 매핑 복원

    2. folio_zero_segment() → 메모리 내용 완전 제거 (데이터 누출 방지)

    보안 중요성: 해제 전 제로화하지 않으면 다른 프로세스가 해당 물리 페이지를 할당받아 잔여 데이터를 읽을 수 있다.


    호출 흐름

    사용자 공간: memfd_secret(O_CLOEXEC)
        │
        ▼
    SYSCALL_DEFINE1(memfd_secret)
        ├─ secretmem_enable / can_set_direct_map() 확인
        ├─ 플래그 검증
        └─ secretmem_file_create()
             ├─ anon_inode_make_secure_inode()     ← LSM 보안 컨텍스트 적용
             ├─ alloc_file_pseudo()                 ← pseudo 파일 할당
             ├─ mapping_set_gfp_mask(GFP_HIGHUSER) ← 커널 할당 제한
             ├─ mapping_set_unevictable()           ← 회수 불가 설정
             └─ atomic_inc(secretmem_users)
    
    사용자 공간: mmap(fd, ...)
        │
        ▼
    secretmem_mmap_prepare()
        ├─ VMA_SHARED 검증
        ├─ VM_LOCKED | VM_DONTDUMP 설정
        ├─ mlock_future_ok() 확인
        └─ vm_ops = &secretmem_vm_ops
    
    사용자 공간: 접근 (페이지 폴트)
        │
        ▼
    secretmem_fault()
        ├─ filemap_lock_folio() 탐색
        ├─ [없으면] folio_alloc(__GFP_ZERO)
        ├─ set_direct_map_invalid_noflush()     ← 커널 매핑 제거
        ├─ filemap_add_folio()                  ← 매핑에 추가
        └─ flush_tlb_kernel_range()            ← TLB 무효화
    
    GUP 경로 (커널 직접 접근 차단):
        ├─ vma_is_secretmem(vma) → -EFAULT     ← gup.c:1219
        └─ secretmem_mapping(mapping) → false   ← gup_fast_folio_allowed():2803
    
    해제 시:
        close(fd)
        └─ secretmem_release()
             └─ atomic_dec(secretmem_users)
    
    folio 해제 시:
        secretmem_free_folio()
        ├─ set_direct_map_default_noflush()    ← direct map 복원
        └─ folio_zero_segment()               ← 데이터 제로화

    조건별 비교

    memfd_secret 플래그별 동작

    플래그허용 여부설명
    `O_CLOEXEC`허용exec() 시 fd 자동 닫기
    `O_RDWR`허용 (기본값)읽기/쓰기 모드
    기타 flags거부 (`-EINVAL`)mode 플래그는 현재 미사용 (0x0)

    아키텍처별 direct map 제어 지원

    아키텍처`can_set_direct_map()``set_direct_map_invalid_noflush()`비고
    x86_64`true` (기본값)`__set_pages_np()`완전 지원
    ARM64조건부조건부`rodata_full` 또는 KFENCE 또는 Realm
    RISC-V`true` (기본값)지원기본 지원
    s390`true` (기본값)지원기본 지원
    LoongArch`true` (기본값)지원기본 지원

    GUP 경로에서의 secretmem 처리

    경로동작코드 위치
    GUP (slow path)`vma_is_secretmem()` → `-EFAULT` 반환`gup.c:1219`
    GUP-fast (order-0 folio)`secretmem_mapping()` → `false` 반환 (거부)`gup.c:2803`
    GUP-fast (large folio)검사 건너뜀 (secretmem은 order-0만 사용)`gup.c:2756`
    GUP-fast (hugetlb)건너뜀 (secretmem 불가)`gup.c:2766`

    secretmem vs 다른 메모리 격리 메커니즘

    특성Secret Memorymlockmemfd_createsealed memfd
    커널 직접 매핑 제거**예**아니오아니오아니오
    GUP 차단**예**아니오아니오아니오
    회수 불가**예**아니오아니오아니오
    코어 덤프 제외**예**아니오아니오아니오
    LSM 보안 컨텍스트**예**아니오아니오아니오
    사용 사례암호화 키 저장스왑 방지임의 파일리소스 보호

    호출 흐름 다이어그램

    Secret Memory 호출 흐름

    자료구조 관계도

    Secret Memory 자료구조 관계도

    관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 03-vma_mmap.html — VMA / mmap
  • 05-page_reclaim.html — 페이지 회수
  • 12-folio.html — Folio / Page Cache
  • 25-mseal.html — Sealing (VMA 봉인)
  • 26-secretmem.html — Secret Memory (이 문서)

  • 참고 자료

  • 커널 소스: mm/secretmem.c (전체 270줄 — 비교적 작고 집중적인 구현)
  • 설계 문서: Documentation/mm/secretmem.rst
  • 시스템 호출: memfd_secret(2) man page
  • 관련 커밋: Mike Rapoport (IBM) — 2021년 초기 구현, Linux 5.14에서 머지
  • 보안 고려사항: anon_inode_make_secure_inode()를 통한 SELinux/AppArmor 컨텍스트 적용