memfd (Memory File Descriptors)

memfd_create()는 파일 시스템에 존재하지 않는 익명 메모리 파일을 생성하는 리눅스 시스콜입니다. tmpfs 기반의 이 메모리 파일은 mmap(), read()/write(), ftruncate() 등 일반 파일 연산을 모두 지원하면서도 디스크에 흔적을 남기지 않습니다. File Sealing을 통해 공유 메모리의 불변성을 보장하고, SCM_RIGHTS를 통해 프로세스(Process) 간 안전한 메모리 교환이 가능합니다. 이 문서에서는 커널 내부 구현부터 보안 고려사항, Wayland/D-Bus 활용, memfd_secret()까지 전 영역을 다룹니다.

전제 조건: VMA / mmap메모리 관리(Memory Management) 개요 문서를 먼저 읽으세요. 가상 메모리(Virtual Memory), 페이지(Page) 할당, 파일 디스크립터(File Descriptor)의 기본 개념을 이해하고 있어야 합니다.
일상 비유: memfd는 이름표만 붙은 메모 용지와 비슷합니다. 서류함(파일 시스템)에 넣지 않아도 되고, 원하는 사람에게 직접 건네줄 수 있으며, "더 이상 수정하지 마세요"라는 봉인(Seal)을 붙여 내용의 불변성을 보장할 수 있습니다. 용지를 다 쓰면 쓰레기통에 버리는 것(close)만으로 완전히 사라집니다.

핵심 요약

  • memfd_create() — 파일 시스템에 존재하지 않는 익명 메모리 파일을 생성하는 시스콜 (Linux 3.17+)
  • File Sealing — 파일 내용/크기의 변경을 영구적으로 금지하는 메커니즘 (fcntl(F_ADD_SEALS))
  • SCM_RIGHTS — Unix 도메인 소켓(Socket)을 통해 파일 디스크립터를 다른 프로세스에 전달하는 방법
  • memfd_secret() — 커널조차 접근할 수 없는 비밀 메모리 영역 생성 (Linux 5.14+)
  • MFD_NOEXEC_SEAL — memfd의 실행 권한을 영구적으로 금지하여 코드 인젝션 공격을 방지 (Linux 6.3+)

단계별 이해

  1. memfd 생성
    memfd_create("name", flags)로 익명 파일 디스크립터를 얻습니다. 이 파일은 어떤 디렉터리에도 존재하지 않습니다.
  2. 크기 설정
    ftruncate(fd, size)로 메모리 파일의 크기를 지정합니다. tmpfs가 배후에서 페이지를 관리합니다.
  3. 데이터 읽기/쓰기
    write()/read() 또는 mmap()으로 데이터를 조작합니다.
  4. 봉인(Seal) 적용
    fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK)로 쓰기/축소를 영구 금지합니다.
  5. 프로세스 간 공유
    sendmsg()SCM_RIGHTS로 다른 프로세스에 fd를 전달합니다. 수신 측은 봉인 상태를 확인하여 안전하게 mmap할 수 있습니다.

memfd 개요

파일 시스템 없는 메모리 파일

전통적으로 리눅스에서 프로세스 간 공유 메모리를 만들려면 shm_open()(POSIX 공유 메모리), shmget()(System V 공유 메모리), 또는 /tmp에 파일을 만들어 mmap()하는 방법을 사용했습니다. 그러나 이들은 모두 파일 시스템에 이름이 남거나, 수명 관리가 복잡하거나, 보안 문제(예: /dev/shm에 누구나 접근 가능)가 있었습니다.

memfd_create()는 Linux 3.17(2014)에 도입된 시스콜로, 이 모든 문제를 해결합니다. 이 시스콜이 반환하는 파일 디스크립터는 tmpfs(shmem) 위에 존재하지만 어떤 디렉터리에도 링크되지 않습니다. 마지막 참조가 닫히면 커널이 자동으로 메모리를 회수합니다.

기존 공유 메모리 방식과의 비교

방식파일 시스템 이름File Sealing수명 관리보안
System V shm (shmget) IPC 키/ID 불가 명시적 shmctl(IPC_RMID) IPC 키 추측 공격 가능
POSIX shm (shm_open) /dev/shm/name 불가 명시적 shm_unlink() 경로 알면 접근 가능
/tmp + mmap 디스크 파일 경로 불가 명시적 unlink() 경로 알면 접근 가능
memfd_create 없음 (익명) 가능 자동 (refcount) fd 전달로만 공유

memfd_create 시그니처

#include <sys/mman.h>

int memfd_create(const char *name, unsigned int flags);

/* 반환값: 성공 시 파일 디스크립터, 실패 시 -1 (errno 설정)
 * name: /proc/self/fd/N에 표시되는 디버깅용 이름 (경로 아님)
 * flags: MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_HUGETLB | MFD_NOEXEC_SEAL 등 */
코드 설명
  • 1행 sys/mman.h 헤더에 memfd_create 래퍼와 MFD_* 상수가 정의되어 있습니다.
  • 3행 name은 최대 249바이트이며, /proc/<pid>/fd/<N> 심볼릭 링크에 memfd:name 형태로 표시됩니다.
  • 5-7행 flags 조합으로 close-on-exec, sealing 허용, 실행 금지 등을 지정합니다.

memfd_create 시스콜 아키텍처

memfd_create()의 커널 내부 호출 흐름은 다음과 같습니다. 사용자 공간(User Space)에서 시스콜을 호출하면, 커널은 tmpfs(shmem)에 익명 inode를 만들고, 이를 위한 struct file을 할당한 뒤 파일 디스크립터 번호를 반환합니다.

사용자 공간 (User Space) memfd_create("buf", MFD_CLOEXEC) syscall(__NR_memfd_create) 커널 공간 (Kernel Space) 1. 플래그 검증 MFD_* 비트 유효성 2. shmem_file_setup() tmpfs inode + file 할당 3. get_unused_fd_flags() fd 번호 할당 4. Sealing 초기화 inode->i_flags 설정 5. noexec 처리 MFD_NOEXEC_SEAL 검사 6. fd_install() fd 테이블에 file 연결 return fd; (예: 3) /proc/<pid>/fd/3 -> /memfd:buf (deleted) | /proc/<pid>/fdinfo/3: seals: 0x0

커널 소스: __memfd_create 핵심 경로

/* mm/memfd.c - Linux 6.x */
SYSCALL_DEFINE2(memfd_create,
    const char __user *, uname,
    unsigned int, flags)
{
    struct file *file;
    int fd, error;
    char *name;
    unsigned int *file_seals;

    /* 1. 플래그 유효성 검사 */
    if (flags & ~(MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_HUGETLB |
                  MFD_NOEXEC_SEAL | MFD_EXEC))
        return -EINVAL;

    /* 2. 사용자 공간에서 이름 복사 */
    name = strndup_user(uname, MFD_NAME_MAX_LEN + 1);

    /* 3. tmpfs(shmem)에 익명 파일 생성 */
    if (flags & MFD_HUGETLB)
        file = hugetlb_file_setup(name, 0, ...);
    else
        file = shmem_file_setup(name, 0, VM_NORESERVE);

    /* 4. sealing 초기 상태 설정 */
    if (flags & MFD_ALLOW_SEALING)
        file_seals = &(SHMEM_I(file_inode(file)))->seals;

    /* 5. noexec 처리 */
    if (flags & MFD_NOEXEC_SEAL)
        file->f_mode &= ~FMODE_EXEC;

    /* 6. fd 할당 및 설치 */
    fd = get_unused_fd_flags(
        (flags & MFD_CLOEXEC) ? O_CLOEXEC : 0);
    fd_install(fd, file);

    return fd;
}
코드 설명
  • 2-4행 SYSCALL_DEFINE2 매크로(Macro)로 2개 인자(name, flags)를 받는 시스콜을 정의합니다.
  • 12-14행 유효하지 않은 플래그 비트가 있으면 -EINVAL을 반환합니다. 미래 확장을 위한 방어 코드입니다.
  • 20-23행 shmem_file_setup()이 핵심입니다. tmpfs 수퍼블록에 새 inode를 만들고 struct file을 할당합니다.
  • 26-27행 MFD_ALLOW_SEALING 플래그가 있으면 shmem inode의 seals 필드에 접근 가능하도록 설정합니다.
  • 30-31행 MFD_NOEXEC_SEAL이 설정되면 FMODE_EXEC를 제거하여 이 파일의 실행을 영구 차단합니다.
  • 34-36행 get_unused_fd_flags()로 빈 fd 번호를 확보하고, fd_install()로 프로세스의 fd 테이블에 등록합니다.

memfd_create() 호출 체인 분석

memfd_create() 시스콜이 커널 내부에서 실제로 어떤 함수들을 거쳐 파일을 생성하는지 호출 체인(Call Chain)을 추적합니다. 핵심 경로는 sys_memfd_create()에서 시작하여 shmem_file_setup()으로 tmpfs 파일을 만들고, 내부적으로 shmem_get_inode()alloc_file_pseudo()를 거쳐 완전한 struct file을 반환합니다.

memfd_create() 커널 내부 호출 체인 sys_memfd_create() mm/memfd.c — 플래그 검증, 이름 복사 MFD_HUGETLB? Yes No (일반) hugetlb_file_setup() hugetlbfs 수퍼블록 사용 shmem_file_setup() mm/shmem.c — tmpfs 파일 생성 진입점 __shmem_file_setup() kern_mount 수퍼블록 획득, inode + file 할당 shmem_get_inode() new_inode() + shmem_inode_info 초기화 seals = F_SEAL_SEAL (기본값) alloc_file_pseudo() struct file 할당 f_op = shmem_file_operations inode struct file* 반환 → fd_install(fd, file) file->f_inode->i_mapping = address_space (페이지 캐시용 xarray 포함)

memfd_create() 함수 구현 분석

커널 소스 mm/memfd.cmemfd_create() 전체 흐름을 단순화하여 분석합니다. 플래그 유효성 검사, hugetlb와 shmem 분기, noexec 처리, sealing 초기화까지의 전 과정을 포함합니다.

/* mm/memfd.c - Linux 6.x 단순화 */
SYSCALL_DEFINE2(memfd_create,
    const char __user *, uname,
    unsigned int, flags)
{
    unsigned int *file_seals;
    struct file *file;
    int fd, error;
    char *name;

    /* 1. 유효하지 않은 플래그 비트 검사 */
    if (flags & ~(MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_HUGETLB |
                  MFD_NOEXEC_SEAL | MFD_EXEC |
                  MFD_HUGE_MASK))
        return -EINVAL;

    /* 2. MFD_NOEXEC_SEAL과 MFD_EXEC 동시 사용 불가 */
    if ((flags & MFD_EXEC) && (flags & MFD_NOEXEC_SEAL))
        return -EINVAL;

    /* 3. vm.memfd_noexec sysctl에 따른 강제 정책 */
    error = check_sysctl_memfd_noexec(&flags);
    if (error < 0)
        return error;

    /* 4. 사용자 공간에서 이름 복사 (최대 249+1 바이트) */
    name = strndup_user(uname, MFD_NAME_MAX_LEN + 1);
    if (IS_ERR(name))
        return PTR_ERR(name);

    /* 5. fd 번호 미리 확보 */
    fd = get_unused_fd_flags(
        (flags & MFD_CLOEXEC) ? O_CLOEXEC : 0);

    /* 6. hugetlb vs shmem 분기 - 핵심 파일 생성 */
    if (flags & MFD_HUGETLB) {
        struct user_struct *user = get_current_user();
        file = hugetlb_file_setup(name, 0,
            VM_NORESERVE, user,
            HUGETLB_ANONHUGE_INODE,
            (flags >> MFD_HUGE_SHIFT) & MFD_HUGE_MASK);
    } else {
        file = shmem_file_setup(name, 0, VM_NORESERVE);
    }

    /* 7. MFD_ALLOW_SEALING: seals 잠금 해제 */
    file_seals = memfd_file_seals_ptr(file);
    if (file_seals && (flags & MFD_ALLOW_SEALING))
        *file_seals &= ~F_SEAL_SEAL;  /* 기본 F_SEAL_SEAL 제거 */

    /* 8. noexec 처리: FMODE_EXEC 제거 + F_SEAL_EXEC 적용 */
    if (flags & MFD_NOEXEC_SEAL) {
        file->f_mode &= ~FMODE_EXEC;
        if (file_seals)
            *file_seals |= F_SEAL_EXEC;
    } else if (flags & MFD_EXEC) {
        file->f_mode |= FMODE_EXEC;
    }

    /* 9. 프로세스 fd 테이블에 설치 */
    fd_install(fd, file);
    kfree(name);
    return fd;
}
코드 설명
  • 11-15행 알려진 MFD_* 플래그 이외의 비트가 설정되면 -EINVAL을 반환합니다. MFD_HUGE_MASK로 hugetlb 크기 인코딩 비트도 허용합니다.
  • 18-19행 MFD_EXEC(실행 허용)과 MFD_NOEXEC_SEAL(실행 금지)은 상호 배타적이므로 동시 사용 시 -EINVAL을 반환합니다.
  • 22-24행 vm.memfd_noexec sysctl 값(0/1/2)에 따라 MFD_EXEC 미지정 시 자동으로 MFD_NOEXEC_SEAL을 추가하거나, MFD_EXEC 사용을 거부합니다.
  • 35-43행 MFD_HUGETLB 플래그에 따라 hugetlb_file_setup()(hugetlbfs) 또는 shmem_file_setup()(tmpfs) 경로로 분기합니다. 초기 크기는 0이며, ftruncate()로 나중에 설정합니다.
  • 46-48행 shmem_get_inode()에서 기본으로 F_SEAL_SEAL이 설정되어 sealing이 금지됩니다. MFD_ALLOW_SEALING 플래그가 있으면 이 seal을 제거하여 sealing을 허용합니다.
  • 51-56행 MFD_NOEXEC_SEALFMODE_EXEC를 제거하고 F_SEAL_EXEC를 적용하여 실행 권한을 영구적으로 차단합니다. MFD_EXEC는 반대로 실행을 명시적으로 허용합니다.

memfd 내부 구현 (tmpfs 기반)

memfd의 배후 저장소는 tmpfs(shmem)입니다. memfd로 생성된 파일은 커널 내부의 tmpfs 인스턴스에 inode가 할당되지만, 어떤 디렉터리 엔트리(dentry)에도 연결되지 않습니다. 이러한 "연결 해제된(unlinked)" 상태가 memfd의 핵심 특성입니다.

프로세스 fd 테이블 fd 0 -> stdin fd 1 -> stdout fd 2 -> stderr fd 3 -> memfd fd 4 -> ... struct file f_op = shmem_file_ops f_inode -> inode f_mode = FMODE_READ | FMODE_WRITE f_mapping -> mapping shmem_inode_info vfs_inode (struct inode) seals = F_SEAL_SEAL i_mapping (address_space) swapped = 0 fallocend = 0 i_nlink = 0 (unlinked) xarray: 페이지 캐시 tmpfs 내부 수퍼블록 (kern_mount) s_fs_info -> shmem_sb_info | s_type = shmem_fs_type | 디렉터리 엔트리 없음 물리 페이지 (Buddy Allocator) address_space의 xarray를 통해 offset -> page 매핑 | 페이지 폴트 시 shmem_getpage_gfp()로 할당

핵심 커널 자료구조 관계

/* include/linux/shmem_fs.h */
struct shmem_inode_info {
    spinlock_t          lock;
    unsigned int        seals;       /* File Sealing 비트마스크 */
    unsigned long       flags;
    unsigned long       alloced;     /* 할당된 페이지 수 */
    unsigned long       swapped;     /* swap된 페이지 수 */
    pgoff_t             fallocend;   /* fallocate 끝 오프셋 */
    struct shared_policy policy;     /* NUMA 정책 */
    struct simple_xattrs xattrs;
    struct inode        vfs_inode;   /* 내장 VFS inode */
};

/* container_of 매크로로 vfs_inode에서 shmem_inode_info 접근 */
#define SHMEM_I(inode) \
    container_of(inode, struct shmem_inode_info, vfs_inode)

shmem_file_operations

/* mm/shmem.c */
static const struct file_operations shmem_file_operations = {
    .mmap           = shmem_mmap,
    .get_unmapped_area = shmem_get_unmapped_area,
    .llseek         = shmem_file_llseek,
    .read_iter      = shmem_file_read_iter,
    .write_iter     = generic_file_write_iter,
    .fsync          = noop_fsync,          /* 디스크 없으므로 no-op */
    .splice_read    = shmem_file_splice_read,
    .splice_write   = iter_file_splice_write,
    .fallocate      = shmem_fallocate,
};

shmem_fault(): 페이지 폴트 경로

memfd의 mmap 매핑에 처음 접근하면 페이지 폴트(Page Fault)가 발생합니다. 이때 커널의 shmem_fault() 함수가 호출되어 실제 물리 페이지를 할당합니다. 이 지연 할당(Lazy Allocation) 방식 덕분에 ftruncate()로 큰 크기를 설정해도 즉시 메모리가 소비되지 않습니다.

memfd 페이지 폴트 처리 경로 ptr[0] = 'A'; (첫 쓰기 접근) PTE 미존재 → #PF handle_mm_fault() shmem_fault(vmf) vm_ops->fault = shmem_fault (MAP_SHARED) shmem_getpage_gfp(inode, index, ...) xarray에서 기존 페이지 검색 → 없으면 새 페이지 할당 캐시 히트 캐시 미스 xarray에서 page 반환 xa_load(mapping->i_pages, index) shmem_alloc_and_add_folio() alloc_pages() + xa_store() + memcg 과금 swap 엔트리 shmem_swapin_folio() swap에서 페이지 복구 vmf->page = folio_page(folio, 0) PTE 설치 → 사용자 접근 재개
/* mm/shmem.c - shmem_fault() 단순화 */
static vm_fault_t shmem_fault(
    struct vm_fault *vmf)
{
    struct inode *inode = file_inode(vmf->vma->vm_file);
    pgoff_t index = vmf->pgoff;
    struct folio *folio;
    int err;

    /* 1. shmem_getpage_gfp() 호출:
     *    xarray 검색 → 히트면 반환
     *    swap 엔트리면 swapin
     *    둘 다 아니면 새 folio 할당 */
    err = shmem_getpage_gfp(inode, index,
        &folio, SGP_CACHE,
        vmf->gfp_mask, vmf);

    if (err)
        return vmf_error(err);

    /* 2. fault 구조체에 페이지 설정
     *    → 상위 코드가 PTE를 설치 */
    vmf->page = folio_page(folio, index - folio->index);
    return VM_FAULT_LOCKED;
}
코드 설명
  • 5-6행 vmf->vma->vm_file에서 inode를 추출하고, vmf->pgoff가 접근된 파일 오프셋의 페이지 인덱스입니다.
  • 14-16행 shmem_getpage_gfp()가 핵심입니다. SGP_CACHE 모드는 기존 페이지가 없으면 새로 할당합니다. xarray에서 먼저 검색하고, swap 엔트리가 있으면 shmem_swapin_folio()로 복구합니다.
  • 22-23행 folio에서 정확한 page를 추출하여 vmf->page에 설정합니다. VM_FAULT_LOCKED를 반환하면 상위 코드가 PTE를 설치합니다.

memfd와 swap 상호작용

memfd 페이지는 tmpfs(shmem) 기반이므로 swap 대상입니다. 메모리 압박(Memory Pressure) 시 커널의 페이지 회수(Reclaim) 경로가 memfd 페이지를 swap out할 수 있습니다. 이것은 MAP_ANONYMOUS | MAP_PRIVATE의 익명 페이지가 swap out되는 것과 유사하지만, shmem 특유의 swap 엔트리 관리가 적용됩니다.

memfd 페이지의 swap 생명주기 활성 (Active) xarray에 folio 저장 PTE가 물리 페이지 참조 kswapd / direct reclaim Swap Out xarray에 swap 엔트리 저장 info->swapped++ 접근 시 swapin Swap In (복구) shmem_swapin_folio() zswap 캐시 RAM 내 압축 보관 디스크 I/O 회피 zswap 활성 시 memfd swap 핵심 포인트 • shmem 페이지는 xarray에 swap 엔트리(swp_entry_t)로 대체됨 (일반 anon page의 PTE swap 엔트리와 다름) • info->swapped 카운터로 현재 swap된 페이지 수 추적 • mlock()된 매핑은 swap 대상에서 제외됨 → 성능 민감 memfd에 mlockall() 고려
/* mm/shmem.c - shmem swap out 핵심 경로 (단순화) */
static int shmem_writepage(
    struct page *page,
    struct writeback_control *wbc)
{
    struct shmem_inode_info *info;
    swp_entry_t swap;

    /* 1. swap 슬롯 할당 */
    swap = get_swap_page(page);
    if (!swap.val)
        return -ENOMEM;  /* swap 공간 부족 */

    /* 2. xarray에서 page를 swap 엔트리로 교체 */
    info = SHMEM_I(page->mapping->host);
    xa_store(&page->mapping->i_pages,
        page->index, swp_to_radix_entry(swap),
        GFP_ATOMIC);

    /* 3. swap된 페이지 수 증가 */
    info->swapped++;

    /* 4. 물리 페이지를 swap 장치에 기록 */
    swap_writepage(page, wbc);

    return 0;
}
코드 설명
  • 10-12행 get_swap_page()로 swap 슬롯을 확보합니다. 실패하면 이 페이지는 swap할 수 없으므로 회수를 포기합니다.
  • 16-18행 xarray의 해당 인덱스에 저장된 page 포인터를 swp_entry_t로 교체합니다. 이후 이 인덱스에 접근하면 swap 엔트리가 발견되어 swapin이 트리거됩니다.
  • 21행 info->swapped 카운터를 증가시켜 현재 swap된 페이지 수를 추적합니다. /proc/meminfo의 Shmem 통계에 반영됩니다.

cgroup/memcg 메모리 과금

memfd 페이지는 이를 생성한 프로세스의 memory cgroup(memcg)에 과금됩니다. 구체적으로, shmem_getpage_gfp()에서 새 folio를 할당할 때 mem_cgroup_charge()가 호출되어 해당 cgroup의 memory.current에 반영됩니다.

시점과금 동작관련 함수
페이지 할당 (폴트) 생성 프로세스의 memcg에 과금 mem_cgroup_charge()
fd 전달 후 수신 측 접근 원래 생성자의 memcg에 과금 유지 이미 과금된 페이지를 공유
swap out swap 카운트로 이동 mem_cgroup_uncharge() + swap accounting
close (마지막 참조 해제) 모든 페이지 해제, 과금 해제 shmem_evict_inode()truncate_inode_pages()
주의: memfd를 생성한 프로세스 A가 fd를 프로세스 B에 전달한 후 종료해도, B가 fd를 보유하는 한 메모리는 해제되지 않습니다. 그러나 과금은 이미 사라진 A의 cgroup에 남아 좀비 과금(zombie charging)이 될 수 있습니다. 컨테이너 환경에서 메모리 리미트(Limit) 계산 시 이 점에 유의해야 합니다.
# memfd가 속한 cgroup의 메모리 사용량 확인
cat /sys/fs/cgroup/user.slice/user-1000.slice/memory.current
cat /sys/fs/cgroup/user.slice/user-1000.slice/memory.stat | grep shmem
# shmem 항목에 memfd 페이지 사용량이 포함됩니다

# 특정 프로세스가 보유한 memfd의 메모리 크기 확인
for fd in /proc/$PID/fd/*; do
  target=$(readlink "$fd" 2>/dev/null)
  if [[ "$target" == *memfd* ]]; then
    echo "$fd -> $target ($(stat -c%s /proc/$PID/fd/$(basename $fd)) bytes)"
  fi
done

MFD_CLOEXEC, MFD_ALLOW_SEALING 플래그

memfd_create()flags 인자로 전달할 수 있는 플래그들과 각각의 의미를 정리합니다.

플래그도입 버전설명
MFD_CLOEXEC 0x0001 3.17 exec() 시 자동 close. O_CLOEXEC과 동일 효과. 거의 항상 설정 권장
MFD_ALLOW_SEALING 0x0002 3.17 File Sealing을 허용. 이 플래그 없이는 fcntl(F_ADD_SEALS)-EPERM 반환
MFD_HUGETLB 0x0004 4.14 hugetlbfs 기반 메모리 파일 생성. MFD_HUGE_2MB, MFD_HUGE_1GB 등과 OR 조합
MFD_NOEXEC_SEAL 0x0008 6.3 실행 권한 영구 차단 + F_SEAL_EXEC 적용. 보안 강화 용도
MFD_EXEC 0x0010 6.3 실행 가능한 memfd 생성. vm.memfd_noexec sysctl과 상호작용

플래그 사용 예시

/* 가장 일반적인 조합: close-on-exec + sealing 허용 */
int fd = memfd_create("shared-buf",
    MFD_CLOEXEC | MFD_ALLOW_SEALING);

/* 보안 강화: 실행 불가 + sealing */
int fd_safe = memfd_create("safe-buf",
    MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL);

/* 2MB 휴즈페이지 기반 대용량 버퍼 */
int fd_huge = memfd_create("huge-buf",
    MFD_CLOEXEC | MFD_HUGETLB | MFD_HUGE_2MB);
모범 사례: 새로운 코드에서는 항상 MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL을 기본으로 사용하세요. JIT 컴파일러처럼 실행 권한이 필요한 경우에만 MFD_EXEC를 사용합니다.

플래그 비트 정의와 커널 자료구조

memfd_create()에 전달되는 플래그들은 커널 헤더 include/uapi/linux/memfd.h에 정의되어 있습니다. 각 플래그 비트가 커널 내부에서 어떤 자료구조 필드에 반영되는지 추적합니다.

/* include/uapi/linux/memfd.h */
#define MFD_CLOEXEC         0x0001U  /* → get_unused_fd_flags(O_CLOEXEC) */
#define MFD_ALLOW_SEALING  0x0002U  /* → shmem_inode_info.seals &= ~F_SEAL_SEAL */
#define MFD_HUGETLB       0x0004U  /* → hugetlb_file_setup() 경로 선택 */
#define MFD_NOEXEC_SEAL   0x0008U  /* → file->f_mode &= ~FMODE_EXEC */
#define MFD_EXEC          0x0010U  /* → file->f_mode |= FMODE_EXEC */

/* hugetlb 크기 인코딩 (MFD_HUGETLB와 OR 조합) */
#define MFD_HUGE_SHIFT    26
#define MFD_HUGE_MASK     0x3F
#define MFD_HUGE_2MB     (21 << MFD_HUGE_SHIFT)
#define MFD_HUGE_1GB     (30 << MFD_HUGE_SHIFT)

/* F_SEAL_* 플래그 — include/uapi/linux/fcntl.h */
#define F_SEAL_SEAL          0x0001  /* 추가 seal 적용 차단 */
#define F_SEAL_SHRINK        0x0002  /* 파일 축소 차단 */
#define F_SEAL_GROW          0x0004  /* 파일 확장 차단 */
#define F_SEAL_WRITE         0x0008  /* 쓰기 차단 (기존 mmap 포함) */
#define F_SEAL_FUTURE_WRITE  0x0010  /* 새 쓰기 차단 (기존 mmap 유지) */
#define F_SEAL_EXEC          0x0020  /* 실행 권한 변경 차단 (6.3+) */
코드 설명
  • 2행 MFD_CLOEXECget_unused_fd_flags()O_CLOEXEC를 전달하여, exec() 시 자동으로 fd가 닫히도록 합니다.
  • 3행 MFD_ALLOW_SEALINGshmem_inode_info.seals에서 기본 F_SEAL_SEAL을 제거합니다. 이 seal이 제거되어야 fcntl(F_ADD_SEALS)가 작동합니다.
  • 4행 MFD_HUGETLB는 tmpfs 대신 hugetlbfs를 사용하여 대용량 페이지(HugePage) 기반 메모리를 할당합니다.
  • 5-6행 MFD_NOEXEC_SEALfile->f_mode에서 FMODE_EXEC를 제거하고 F_SEAL_EXEC를 적용합니다. MFD_EXEC는 반대로 실행을 명시적으로 허용합니다.
  • 8-11행 hugetlb 크기는 flags의 상위 비트에 인코딩됩니다. MFD_HUGE_2MB는 비트 26-31에 21(2^21 = 2MB)을 저장합니다.
  • 14-19행 F_SEAL_* 플래그는 비트마스크로, shmem_inode_info.seals 필드에 OR로 누적됩니다. 한번 설정된 비트는 제거할 수 없습니다.

memfd file_operations 구조체

memfd가 tmpfs 기반일 때 struct filef_op에 설정되는 연산 테이블입니다. 이 구조체의 각 함수 포인터가 read(), write(), mmap() 등 사용자 공간 시스콜을 커널 내부 구현에 연결합니다.

/* mm/shmem.c — memfd/tmpfs 파일 연산 테이블 */
static const struct file_operations shmem_file_operations = {
    .mmap               = shmem_mmap,
    .get_unmapped_area  = shmem_get_unmapped_area,
    .llseek             = shmem_file_llseek,
    .read_iter          = shmem_file_read_iter,
    .write_iter         = generic_file_write_iter,
    .fsync              = noop_fsync,
    .splice_read        = shmem_file_splice_read,
    .splice_write       = iter_file_splice_write,
    .fallocate          = shmem_fallocate,
};

/* fcntl(F_ADD_SEALS/F_GET_SEALS) 처리 경로:
 * sys_fcntl() → do_fcntl() → memfd_fcntl()
 *   → F_ADD_SEALS: shmem_add_seals(file, seals)
 *   → F_GET_SEALS: memfd_file_seals_ptr(file) 반환
 *
 * 핵심: sealing은 file_operations가 아니라
 * fcntl 경로에서 직접 처리됩니다. */
코드 설명
  • 3행 shmem_mmap()mmap() 시 호출되며, 현재 seal 상태를 검사하여 F_SEAL_WRITE가 적용된 경우 PROT_WRITE 매핑을 거부합니다.
  • 6행 shmem_file_read_iter()read()/pread() 시 호출됩니다. 페이지가 아직 할당되지 않았으면 0으로 채운 데이터를 반환합니다.
  • 7행 generic_file_write_iter()는 VFS 공통 쓰기 함수입니다. 내부에서 F_SEAL_WRITE/F_SEAL_GROW seal을 확인합니다.
  • 8행 noop_fsync()는 아무 동작도 하지 않습니다. tmpfs는 디스크가 없으므로 fsync가 의미 없습니다.
  • 14-19행 sealing 관련 fcntl() 호출은 file_operations가 아닌 별도의 memfd_fcntl() 함수에서 처리됩니다. 이 설계를 통해 sealing 로직이 shmem 코드와 분리됩니다.

File Sealing 메커니즘

Seal 개념과 TOCTOU 방지

File Sealing은 memfd의 가장 혁신적인 기능입니다. 일반적으로 공유 메모리를 사용할 때 "생산자가 데이터를 쓴 후 소비자가 읽기 전에 크기를 줄이면 어떡하지?"라는 TOCTOU(Time of Check, Time of Use) 문제가 발생합니다. Seal은 이러한 변경을 커널 수준에서 영구적으로 금지합니다.

초기 상태 seals = 0x0 F_SEAL_WRITE (0x08) write(), mmap(PROT_WRITE) 차단 F_SEAL_SHRINK (0x02) ftruncate(작은 크기) 차단 F_SEAL_GROW (0x04) ftruncate(큰 크기), write(EOF) 차단 F_SEAL_SEAL (0x01) 추가 seal 적용 차단 (잠금) F_SEAL_FUTURE_WRITE (0x10) 새 mmap(W) 차단, 기존 허용 완전 봉인 (Fully Sealed) SEAL | SHRINK | GROW | WRITE = 0x0F Seal은 한 번 적용하면 제거할 수 없습니다 (단방향). F_SEAL_SEAL이 적용되면 더 이상 새로운 seal을 추가할 수 없습니다.

Seal 적용 및 확인 API

#include <fcntl.h>
#include <sys/mman.h>

int fd = memfd_create("sealed", MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, 4096);

/* 데이터 기록 */
write(fd, "Hello, memfd!", 13);

/* Seal 적용: 쓰기 + 크기 변경 금지 */
fcntl(fd, F_ADD_SEALS,
    F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);

/* 현재 seal 상태 확인 */
int seals = fcntl(fd, F_GET_SEALS);
if (seals & F_SEAL_WRITE)
    printf("쓰기 봉인됨\n");

/* 이후 write()는 -EPERM 반환 */
ssize_t ret = write(fd, "fail", 4);
/* ret == -1, errno == EPERM */
주의: F_SEAL_WRITE를 적용하려면 현재 쓰기 가능한 mmap() 매핑(Mapping)이 없어야 합니다. 존재하면 -EBUSY가 반환됩니다. F_SEAL_FUTURE_WRITE(Linux 5.1+)는 기존 매핑은 유지하면서 새로운 쓰기 매핑만 차단합니다.

Sealing 커널 경로: memfd_fcntl() → shmem_add_seals()

사용자 공간에서 fcntl(fd, F_ADD_SEALS, seals)를 호출하면 커널 내부에서 memfd_fcntl()을 거쳐 shmem_add_seals()가 실행됩니다. 이 함수가 seal 비트를 실제로 shmem_inode_info.seals 필드에 적용하는 핵심 로직입니다.

fcntl(F_ADD_SEALS) 커널 내부 경로 fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK) do_fcntl() memfd_fcntl(file, cmd, seals) mm/memfd.c — F_ADD_SEALS / F_GET_SEALS 분기 shmem_add_seals(file, seals) mm/shmem.c — seal 유효성 검사 + 적용 inode_lock → seal 충돌 검사 → 쓰기 매핑 확인 → seals |= new_seals F_SEAL_SEAL 검사 이미 설정 시 -EPERM 반환 쓰기 매핑 검사 PROT_WRITE mmap 존재 시 -EBUSY seals |= new_seals shmem_inode_info.seals 갱신

shmem_add_seals() 함수 구현 분석

shmem_add_seals()는 seal 적용의 핵심 함수입니다. inode 락을 잡고, 현재 seal 상태와 충돌하는지 검사한 후, F_SEAL_WRITE 적용 시에는 기존의 쓰기 가능한 mmap 매핑이 없는지까지 확인합니다.

/* mm/shmem.c - Linux 6.x 단순화 */
int shmem_add_seals(struct file *file, unsigned int seals)
{
    struct inode *inode = file_inode(file);
    struct shmem_inode_info *info = SHMEM_I(inode);
    int error;

    /* 1. 쓰기 권한이 있는 fd인지 확인 */
    if (!(file->f_mode & FMODE_WRITE))
        return -EPERM;

    inode_lock(inode);

    /* 2. F_SEAL_SEAL이 이미 적용되어 있으면 추가 seal 불가 */
    if (info->seals & F_SEAL_SEAL) {
        error = -EPERM;
        goto unlock;
    }

    /* 3. F_SEAL_WRITE 요청 시: 기존 쓰기 매핑 검사 */
    if ((seals & F_SEAL_WRITE) &&
        !(info->seals & F_SEAL_WRITE)) {
        error = mapping_deny_writable(file->f_mapping);
        if (error)    /* 쓰기 매핑 존재 → -EBUSY */
            goto unlock;
    }

    /* 4. seal 비트를 OR로 누적 (제거는 불가) */
    info->seals |= seals;
    error = 0;

unlock:
    inode_unlock(inode);
    return error;
}
코드 설명
  • 4-5행 file_inode()으로 struct inode를 가져오고, SHMEM_I() 매크로(container_of)로 shmem_inode_info에 접근합니다.
  • 8-10행 seal을 적용하려면 fd에 쓰기 권한(FMODE_WRITE)이 있어야 합니다. 읽기 전용 fd로는 seal 추가가 불가능합니다.
  • 12행 inode_lock()으로 mutex를 잡아 동시 seal 적용의 경합(Race Condition)을 방지합니다.
  • 15-18행 F_SEAL_SEAL이 이미 적용되어 있으면 어떤 새로운 seal도 추가할 수 없습니다. 이것이 "seal의 seal"이라 불리는 이유입니다.
  • 21-26행 F_SEAL_WRITE 적용 시 mapping_deny_writable()로 현재 쓰기 가능한 mmap() 매핑이 있는지 확인합니다. 존재하면 -EBUSY를 반환하여 데이터 무결성을 보장합니다.
  • 29행 seal 비트를 OR 연산으로 누적합니다. 비트를 제거하는 인터페이스는 존재하지 않으므로 seal은 단방향(단조 증가)입니다.

F_SEAL_FUTURE_WRITE: 기존 매핑과 새 매핑의 분리

F_SEAL_WRITE는 모든 쓰기를 차단하므로, 기존에 PROT_WRITE로 mmap된 매핑이 있으면 -EBUSY를 반환합니다. 이를 해결하기 위해 Linux 5.1에 도입된 F_SEAL_FUTURE_WRITE기존 매핑의 쓰기는 유지하면서 새로운 쓰기 매핑만 차단합니다.

F_SEAL_WRITE vs F_SEAL_FUTURE_WRITE F_SEAL_WRITE write(fd, ...) → -EPERM 새 mmap(PROT_WRITE) → -EPERM 기존 mmap(PROT_WRITE) → 적용 전 해제 필수! 적용 조건 mapping_deny_writable() 성공 필요 → 쓰기 매핑 존재 시 -EBUSY F_SEAL_FUTURE_WRITE write(fd, ...) → -EPERM 새 mmap(PROT_WRITE) → -EPERM 기존 mmap(PROT_WRITE) → 계속 쓰기 가능! 적용 조건 mapping_deny_writable() 호출 안 함 → 기존 쓰기 매핑이 있어도 적용 가능
/* F_SEAL_FUTURE_WRITE 사용 패턴:
 * 생산자가 mmap으로 계속 쓰면서, 소비자에게는 읽기만 허용 */

int fd = memfd_create("live-buf",
    MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, 4096 * 64);

/* 생산자: 쓰기 매핑을 먼저 생성 */
void *w = mmap(NULL, 4096 * 64,
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

/* FUTURE_WRITE seal 적용:
 * 기존 w 매핑은 계속 쓰기 가능
 * 소비자가 받은 fd로 새 PROT_WRITE mmap은 불가 */
fcntl(fd, F_ADD_SEALS,
    F_SEAL_FUTURE_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);

/* 소비자에게 fd 전달 → 소비자는 PROT_READ로만 mmap 가능 */
send_fd(sock, fd);

/* 생산자는 계속 w를 통해 업데이트 가능 */
memcpy(w, new_data, new_data_len);
활용 사례: F_SEAL_FUTURE_WRITE는 Chromium의 공유 메모리 리전(SharedMemoryRegion)에서 사용됩니다. 렌더러(Renderer) 프로세스가 버퍼에 계속 쓰면서, 브라우저(Browser) 프로세스에는 읽기 전용 접근만 허용하는 패턴입니다. Android의 ASharedMemory_setProt()도 내부적으로 이 seal을 활용합니다.
/* mm/shmem.c - F_SEAL_FUTURE_WRITE 검사 경로 (shmem_mmap 내부) */
static int shmem_mmap(struct file *file,
    struct vm_area_struct *vma)
{
    struct shmem_inode_info *info = SHMEM_I(file_inode(file));

    /* F_SEAL_WRITE: 모든 쓰기 매핑 거부 */
    if ((info->seals & F_SEAL_WRITE) &&
        (vma->vm_flags & VM_WRITE))
        return -EPERM;

    /* F_SEAL_FUTURE_WRITE: 새 쓰기 매핑 거부
     * 기존 매핑은 이 함수가 호출되지 않으므로 영향 없음 */
    if ((info->seals & F_SEAL_FUTURE_WRITE) &&
        (vma->vm_flags & VM_WRITE))
        return -EPERM;

    /* ... 정상 매핑 진행 ... */
    vma->vm_ops = &shmem_vm_ops;
    return 0;
}

Seal API 사용법

💡

Seal 적용 순서: Seal은 한번 적용하면 제거할 수 없습니다 (단, F_SEAL_SEAL이 없으면 새 seal 추가는 가능). 실무에서 권장하는 순서: ① 데이터 쓰기 완료 → ② F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW 적용 → ③ fd를 소비자에게 전달 → ④ 소비자가 F_GET_SEALS로 seal 확인 후 안전하게 mmap. 이 패턴은 Wayland 컴포지터에서 클라이언트 버퍼(Buffer) 공유에 사용됩니다.

memfd와 프로세스 간 공유

fd 전달 방법

memfd의 핵심 사용 사례는 프로세스 간 메모리 공유입니다. 파일 시스템에 이름이 없으므로, fd를 전달하는 방법은 크게 세 가지입니다:

  1. SCM_RIGHTS: Unix 도메인 소켓의 ancillary data로 fd 전달 (가장 일반적)
  2. pidfd_getfd(): 대상 프로세스의 fd를 직접 복제 (Linux 5.6+)
  3. fork(): 자식 프로세스가 부모의 fd를 상속
생산자 프로세스 (Producer) 1. fd = memfd_create("buf", MFD_ALLOW_SEALING) 2. ftruncate(fd, 65536) 3. ptr = mmap(NULL, 65536, PROT_WRITE, ...) 4. memcpy(ptr, data, len); munmap(ptr, ...) 5. fcntl(fd, F_ADD_SEALS, WRITE|SHRINK|GROW) 6. sendmsg(sock, &msg, 0) [SCM_RIGHTS] 소비자 프로세스 (Consumer) 7. recvmsg(sock, &msg, 0) -> fd 수신 8. seals = fcntl(fd, F_GET_SEALS) 9. seals 검증: WRITE|SHRINK|GROW 확인 10. size = fstat(fd).st_size (안전!) 11. ptr = mmap(NULL, size, PROT_READ, ...) SCM_RIGHTS

SCM_RIGHTS를 이용한 fd 전달 (생산자)

static void send_fd(int sock, int fd)
{
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(sizeof(int))];
    struct iovec iov = { .iov_base = "x", .iov_len = 1 };

    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type  = SCM_RIGHTS;
    cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
    memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));

    sendmsg(sock, &msg, 0);
}

pidfd_getfd()를 이용한 fd 복제 (Linux 5.6+)

/* 대상 프로세스의 fd를 직접 복제 (ptrace 권한 필요) */
int pidfd = pidfd_open(target_pid, 0);
int stolen_fd = pidfd_getfd(pidfd, target_fd, 0);
/* stolen_fd는 현재 프로세스의 새 fd로,
 * target_pid 프로세스의 target_fd와 같은 파일을 참조 */

/* seal 상태 확인 */
int seals = fcntl(stolen_fd, F_GET_SEALS);
printf("seals: 0x%x\n", seals);

memfd와 mmap 연동

memfd는 mmap()과 함께 사용할 때 가장 큰 효과를 발휘합니다. read()/write() 시스콜 오버헤드(Overhead) 없이 메모리를 직접 접근할 수 있으며, 여러 프로세스가 동일한 물리 페이지를 공유합니다.

프로세스 A VMA: 0x7f..a000 mmap(PROT_READ|WRITE) MAP_SHARED, fd=3 프로세스 B VMA: 0x7f..b000 mmap(PROT_READ) MAP_SHARED, fd=5 페이지 테이블 A 페이지 테이블 B tmpfs 물리 페이지 (공유) shmem_inode_info -> address_space -> xarray -> page 동일한 물리 페이지를 가리킴

mmap 활용 패턴

/* memfd를 mmap으로 매핑하여 제로카피 IPC */
int fd = memfd_create("ipc-region",
    MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, 4096 * 16);  /* 64KB */

/* 쓰기 가능 매핑 (생산자) */
void *ptr = mmap(NULL, 4096 * 16,
    PROT_READ | PROT_WRITE,
    MAP_SHARED, fd, 0);

/* 데이터 기록 */
memcpy(ptr, data, data_len);

/* 매핑 해제 후 seal 적용 */
munmap(ptr, 4096 * 16);
fcntl(fd, F_ADD_SEALS,
    F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);

/* fd를 소비자에게 전달 (SCM_RIGHTS)
 * 소비자는 PROT_READ로만 mmap하여 안전하게 접근 */
성능 이점: memfd + mmap을 사용하면 read()/write() 시스콜의 사용자-커널 복사 오버헤드가 사라집니다. 특히 대용량 데이터 전송에서 pipe()나 소켓 기반 IPC보다 월등한 성능을 보입니다.

memfd_secret (비밀 메모리)

개념과 보안 모델

memfd_secret()는 Linux 5.14에 도입된 시스콜로, 커널조차 접근할 수 없는 비밀 메모리 영역을 생성합니다. 암호화(Encryption) 키, 비밀번호 등 민감한 데이터를 보호하는 데 사용됩니다.

memfd_secret vs memfd_create 차이

특성memfd_creatememfd_secret
배후 저장소 tmpfs (shmem) secretmem (전용)
direct map 제거 아니오 예 (핵심!)
커널 접근 가능 (kmap 등) 불가능
/proc/kcore 노출 가능 불가능
hibernation 시 디스크 기록 가능 불가능
다른 프로세스 공유 가능 (SCM_RIGHTS) 불가능
File Sealing 지원 미지원
사용자 프로세스 mmap(secret_fd) -> 0x7f.. 접근 가능 커널 공간 direct map에서 제거됨 접근 불가 다른 프로세스 /proc/kcore, ptrace 접근 불가 물리 메모리 secretmem 전용 페이지 (direct map에서 해제, 잠금 상태) 커널 direct map (PAGE_OFFSET ~ ) 구멍 (hole) PTE 접근 가능 X 접근 차단 매핑 제거됨

memfd_secret 사용 예시

#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <string.h>

/* memfd_secret()은 glibc 래퍼가 없을 수 있음 */
static int memfd_secret_wrapper(unsigned int flags)
{
    return syscall(SYS_memfd_secret, flags);
}

int main(void)
{
    int fd = memfd_secret_wrapper(0);
    if (fd < 0) {
        perror("memfd_secret");
        return 1;
    }

    /* 크기 설정 */
    ftruncate(fd, 4096);

    /* mmap으로만 접근 가능 (read/write 시스콜 불가) */
    char *secret = mmap(NULL, 4096,
        PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    /* 비밀 데이터 저장 */
    memcpy(secret, "super-secret-key-1234", 21);

    /* ... 비밀 데이터 사용 ... */

    /* 사용 후 명시적으로 제로화 */
    explicit_bzero(secret, 4096);
    munmap(secret, 4096);
    close(fd);

    return 0;
}
제한사항: memfd_secret()은 부팅 시 secretmem.enable=1 커널 파라미터가 필요할 수 있습니다. 또한 direct map에서 페이지를 제거하므로 TLB 플러시(Flush) 비용이 발생하며, hibernation을 비활성화할 수 있습니다. 성능이 중요한 대량 할당에는 부적합합니다.

memfd_secret 커널 내부 구현

memfd_secret()의 핵심 보안 기능은 mm/secretmem.c에 구현되어 있습니다. 일반 memfd와 달리 자체 file_operations를 사용하며, 페이지 폴트 시 set_direct_map_invalid_noflush()로 커널의 direct map에서 해당 페이지를 제거합니다.

memfd_secret() 커널 구현: 페이지 폴트 → direct map 제거 sys_memfd_secret(flags) secretmem_file_setup(flags) anon_inode_getfile("secretmem", &secretmem_fops, ...) mmap() 호출 시 secretmem_mmap() vma->vm_ops = &secretmem_vm_ops; vma->vm_flags |= VM_IO 첫 접근 → 페이지 폴트 secretmem_fault(vmf) 핵심: 페이지 할당 + direct map 제거 + GFP_HIGHUSER alloc_pages(GFP_HIGHUSER) 일반 물리 페이지 할당 set_direct_map_invalid_noflush() 커널 direct map PTE를 무효화 flush_tlb_kernel_range() — TLB 플러시 vmf->page = page → 사용자 PTE만 설치
/* mm/secretmem.c - Linux 6.x 단순화 */
static vm_fault_t secretmem_fault(
    struct vm_fault *vmf)
{
    struct address_space *mapping = vmf->vma->vm_file->f_mapping;
    pgoff_t index = vmf->pgoff;
    struct folio *folio;
    int err;

    /* 1. 기존 페이지 검색 (xarray) */
    folio = filemap_get_folio(mapping, index);
    if (IS_ERR(folio)) {
        /* 2. 새 페이지 할당 */
        folio = filemap_alloc_folio(
            GFP_HIGHUSER, 0);

        /* 3. 핵심: direct map에서 제거!
         *    이후 커널은 이 물리 주소에 접근 불가 */
        err = set_direct_map_invalid_noflush(
            &folio->page);
        if (err)
            goto err_page;

        /* 4. xarray에 저장 */
        filemap_add_folio(mapping, folio, index,
            GFP_KERNEL);
    }

    /* 5. 사용자 공간 PTE에만 매핑 */
    vmf->page = folio_page(folio, 0);
    return VM_FAULT_LOCKED;
}
코드 설명
  • 14-15행 GFP_HIGHUSER로 할당하여 HIGHMEM 영역 페이지를 선호합니다. direct map에서 제거할 것이므로 ZONE_NORMAL 페이지를 낭비하지 않습니다.
  • 19-20행 set_direct_map_invalid_noflush()가 핵심입니다. 이 함수는 커널의 identity map(직접 매핑)에서 해당 물리 페이지의 PTE를 무효화합니다. 이후 커널 코드가 이 주소에 접근하면 page fault가 발생합니다.
  • 30행 사용자 공간의 페이지 테이블에만 PTE가 설치됩니다. 커널의 direct map에는 구멍(hole)이 생겨, /proc/kcore, ptrace, kmap() 등으로도 접근이 불가능합니다.

secretmem 페이지 해제와 direct map 복구

memfd_secret fd가 닫히면 secretmem_release()가 호출되어 모든 비밀 페이지를 제로화(zeroing)하고, set_direct_map_default_noflush()로 direct map을 복구한 뒤 페이지를 Buddy Allocator에 반환합니다.

/* mm/secretmem.c - 해제 경로 (단순화) */
static void secretmem_cleanup_folio(
    struct address_space *mapping,
    struct folio *folio)
{
    /* 1. direct map을 일시적으로 복구하여 접근 가능하게 */
    set_direct_map_default_noflush(&folio->page);

    /* 2. 페이지 내용을 0으로 초기화 (비밀 데이터 잔류 방지) */
    clear_highpage(&folio->page);

    /* 3. 페이지 해제 → Buddy Allocator로 반환 */
    /* (상위 코드에서 folio_put() 호출) */
}
아키텍처 의존성: set_direct_map_invalid_noflush()는 아키텍처별로 구현이 다릅니다. x86에서는 커널 페이지 테이블의 해당 PTE를 직접 수정하고, ARM64에서는 __set_memory_valid()를 통해 linear map의 블록 엔트리를 무효화합니다. RISC-V에서도 유사한 메커니즘이 구현되어 있습니다. CONFIG_ARCH_HAS_SET_DIRECT_MAP이 활성화되어야 memfd_secret()을 사용할 수 있습니다.

Wayland / D-Bus에서의 memfd 활용

memfd는 현대 리눅스 데스크탑 스택의 핵심 인프라입니다. Wayland 컴포지터와 클라이언트 간의 그래픽 버퍼 공유, D-Bus의 대용량 메시지 전달에 memfd가 사용됩니다.

Wayland에서의 wl_shm + memfd

Wayland 프로토콜에서 클라이언트(앱)는 wl_shm 인터페이스를 통해 컴포지터와 그래픽 버퍼를 공유합니다. 전통적으로 /dev/shm에 파일을 만들었으나, 현대 구현체(wlroots, Mutter 등)는 memfd를 선호합니다.

/* Wayland 클라이언트: wl_shm 버퍼 생성 */
int fd = memfd_create("wl_shm", MFD_CLOEXEC | MFD_ALLOW_SEALING);
int stride = width * 4;  /* ARGB8888 */
int size = stride * height;

ftruncate(fd, size);

/* 픽셀 데이터를 직접 기록 */
void *pixels = mmap(NULL, size,
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* ... 렌더링 ... */

/* wl_shm_pool 생성 (fd가 컴포지터로 전달됨) */
struct wl_shm_pool *pool =
    wl_shm_create_pool(shm, fd, size);
struct wl_buffer *buffer =
    wl_shm_pool_create_buffer(pool, 0,
        width, height, stride, WL_SHM_FORMAT_ARGB8888);

D-Bus memfd 전송

D-Bus (kdbus, bus1 제안 포함)에서 대용량 메시지를 전달할 때 memfd를 사용하면 소켓 버퍼 복사를 피하고 제로카피에 가까운 성능을 달성할 수 있습니다. DBUS_TYPE_UNIX_FD를 통해 memfd의 파일 디스크립터를 전달합니다.

/* sd-bus를 이용한 memfd 전달 예시 (systemd) */
int fd = memfd_create("dbus-payload",
    MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, payload_size);
write(fd, payload_data, payload_size);

/* seal 적용: 수신 측에서 안전하게 사용 가능 */
fcntl(fd, F_ADD_SEALS,
    F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL);

/* D-Bus 메시지에 fd 첨부 */
sd_bus_message_append(msg, "h", fd);

보안 고려사항

memfd는 강력한 기능이지만, 악용될 수 있는 공격 벡터가 존재합니다. 특히 memfd를 통한 파일리스(fileless) 코드 실행이 주요 보안 위협입니다.

memfd + exec 공격 벡터

공격자가 시스템에 임의 코드를 실행하고자 할 때, 디스크에 파일을 쓰지 않고 memfd를 이용하여 ELF 바이너리를 메모리에 올린 뒤 execve()로 실행할 수 있습니다. 이 기법은 /proc/self/fd/N 경로를 통해 가능합니다.

/* 경고: 이 패턴은 악성코드에서 사용되는 기법입니다
 * 보안 이해를 위한 목적으로만 제시합니다 */

/* 1. memfd 생성 */
int fd = memfd_create("", MFD_CLOEXEC);

/* 2. ELF 바이너리 기록 */
write(fd, elf_payload, elf_size);

/* 3. /proc/self/fd/N을 통해 실행 */
char path[64];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
execve(path, argv, envp);
/* 디스크에 어떤 파일도 생성되지 않음 (파일리스 공격) */

MFD_NOEXEC_SEAL 방어 (Linux 6.3+)

이 공격을 방어하기 위해 Linux 6.3에서 MFD_NOEXEC_SEAL 플래그와 vm.memfd_noexec sysctl이 도입되었습니다.

vm.memfd_noexec 값동작
0 (기본) 하위 호환. MFD_EXEC/MFD_NOEXEC_SEAL 모두 허용, 미지정 시 실행 가능
1 MFD_EXEC/MFD_NOEXEC_SEAL 미지정 시 기본 MFD_NOEXEC_SEAL 적용
2 강제. 모든 memfd에 MFD_NOEXEC_SEAL 강제, MFD_EXEC 거부
# 시스템 전체에서 memfd 실행 차단 (보안 강화)
sysctl -w vm.memfd_noexec=2

# 영구 설정
echo "vm.memfd_noexec = 2" >> /etc/sysctl.d/99-memfd-noexec.conf
보안 권고: 프로덕션 서버에서는 vm.memfd_noexec=1 이상을 설정하세요. JIT 컴파일러(JavaScript V8, Java HotSpot 등)가 없는 환경에서는 vm.memfd_noexec=2를 권장합니다. 컨테이너(Container) 환경에서는 seccomp 프로필로 memfd_createMFD_EXEC 플래그를 차단할 수도 있습니다.

컨테이너/Seccomp 환경에서의 memfd 보안

컨테이너(Container) 환경에서 memfd는 특별한 보안 고려가 필요합니다. 기본적으로 대부분의 컨테이너 런타임(Docker, containerd, CRI-O)은 memfd_create()를 허용하므로, 컨테이너 내부에서 파일리스 공격이 가능합니다.

컨테이너 환경 memfd 보안 레이어 Layer 1: vm.memfd_noexec sysctl (호스트 전역) 값=2로 설정하면 모든 컨테이너에서 MFD_EXEC 사용 불가 Layer 2: Seccomp-BPF 프로필 memfd_create 시스콜의 flags 인자를 검사하여 MFD_EXEC 비트 차단 (Docker 기본 seccomp 프로필에서는 memfd_create 허용됨 — 추가 설정 필요) Layer 3: LSM (SELinux/AppArmor) file_mmap 훅에서 memfd의 PROT_EXEC 매핑 정책 제어 Layer 4: Landlock / prctl(PR_SET_MDWE) Memory-Deny-Write-Execute: W^X 정책 강제 (memfd mmap에도 적용) 다층 방어 각 레이어는 독립적으로 동작하며, 가장 제한적인 정책이 최종 적용됩니다
// Docker seccomp 프로필: memfd_create에서 MFD_EXEC 차단
{
  "names": ["memfd_create"],
  "action": "SCMP_ACT_ALLOW",
  "args": [
    {
      "index": 1,
      "value": 16,
      "op": "SCMP_CMP_MASKED_EQ",
      "comment": "MFD_EXEC(0x10) 비트가 설정된 호출 차단"
    }
  ]
}
# Kubernetes Pod에서 memfd 실행 차단 (sysctl)
apiVersion: v1
kind: Pod
spec:
  securityContext:
    sysctls:
    - name: vm.memfd_noexec
      value: "2"  # 모든 memfd에 MFD_NOEXEC_SEAL 강제

# prctl로 현재 프로세스에 W^X 강제
# (memfd의 PROT_WRITE|PROT_EXEC 동시 매핑 차단)
prctl(PR_SET_MDWE, PR_MDWE_REFUSE_EXEC_GAIN, 0, 0, 0);
user namespace 제한: vm.memfd_noexec sysctl은 init user namespace에서만 설정 가능합니다. 비특권(Unprivileged) 컨테이너의 user namespace에서는 변경할 수 없으며, 호스트의 설정값이 상속됩니다. 따라서 호스트 수준에서의 정책 설정이 중요합니다.

커널 설정과 sysctl

커널 빌드 설정 (Kconfig)

설정기본값설명
CONFIG_MEMFD_CREATE y memfd_create() 시스콜 활성화. CONFIG_TMPFS에 의존
CONFIG_SECRETMEM y memfd_secret() 시스콜 활성화. direct map 조작 지원 필요
CONFIG_TMPFS y tmpfs 파일 시스템 (memfd의 배후 저장소)
CONFIG_HUGETLBFS y (x86_64) MFD_HUGETLB 플래그 지원에 필요

sysctl 매개변수

# memfd 실행 권한 정책 확인/설정
cat /proc/sys/vm/memfd_noexec
sysctl vm.memfd_noexec

# tmpfs 전체 크기 제한 (memfd도 이 제한에 포함)
mount | grep tmpfs
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,size=8G)

# memfd의 현재 사용량 확인 
cat /proc/meminfo | grep Shmem
# Shmem: 총 shmem/tmpfs 사용량 (memfd 포함)

/proc/<pid>/fdinfo 확인

# memfd의 seal 상태 확인
cat /proc/self/fdinfo/3
# pos:    0
# flags:  02
# mnt_id: 26
# ino:    12345
# seals:  0xf    (모든 seal 적용됨)

# memfd 목록 확인
ls -la /proc/self/fd/ | grep memfd
# lrwx------ 1 user user 64 ... 3 -> /memfd:buf (deleted)

성능 특성

memfd의 성능은 tmpfs(shmem)의 성능 특성을 그대로 따릅니다. 페이지 할당은 Buddy Allocator를 통해 이루어지며, swap 가능합니다.

IPC 메커니즘 성능 비교 (64KB 전송 기준, 낮을수록 좋음) memfd+mmap SysV shm pipe UDS send TCP loopback ~0.3us (제로카피) ~0.5us ~2.5us (커널 버퍼 복사 2회) ~3.2us (소켓 버퍼 복사) ~5.8us memfd+mmap은 데이터 복사 없이 페이지 테이블 공유만으로 동작하여 가장 빠릅니다

성능 최적화 팁

최적화방법효과
대용량 버퍼 MFD_HUGETLB | MFD_HUGE_2MB TLB 미스 감소, 페이지 폴트(Page Fault) 횟수 감소
페이지 사전 할당 fallocate(fd, 0, 0, size) 첫 접근 시 페이지 폴트 방지
NUMA 인지 mbind() / set_mempolicy() NUMA 노드 간 접근 지연(Latency) 감소
Seal 적용 시점 모든 쓰기 완료 후 한 번에 seal seal 검사 오버헤드 최소화

벤치마크 코드

#include <sys/mman.h>
#include <time.h>
#include <stdio.h>
#include <unistd.h>

static double bench_memfd_mmap(size_t size, int iterations)
{
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);

    for (int i = 0; i < iterations; i++) {
        int fd = memfd_create("bench", MFD_CLOEXEC);
        ftruncate(fd, size);

        void *p = mmap(NULL, size,
            PROT_READ | PROT_WRITE,
            MAP_SHARED, fd, 0);
        ((volatile char *)p)[0] = 1;  /* 페이지 폴트 유발 */
        munmap(p, size);
        close(fd);
    }

    clock_gettime(CLOCK_MONOTONIC, &end);
    double elapsed = (end.tv_sec - start.tv_sec)
        + (end.tv_nsec - start.tv_nsec) / 1e9;
    return elapsed / iterations * 1e6; /* 마이크로초 */
}

실전 사용 사례

1. IPC: 구조화된 데이터 공유

/* 헤더 + 가변 길이 데이터를 memfd로 공유 */
struct shared_header {
    uint32_t magic;
    uint32_t version;
    uint64_t data_offset;
    uint64_t data_len;
};

int fd = memfd_create("structured-ipc",
    MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL);
size_t total = sizeof(struct shared_header) + data_len;
ftruncate(fd, total);

void *base = mmap(NULL, total,
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

struct shared_header *hdr = base;
hdr->magic       = 0xDEADBEEF;
hdr->version     = 1;
hdr->data_offset = sizeof(*hdr);
hdr->data_len    = data_len;
memcpy((char *)base + hdr->data_offset, payload, data_len);

munmap(base, total);
fcntl(fd, F_ADD_SEALS,
    F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL);
/* 이제 fd를 SCM_RIGHTS로 전달 */

2. 그래픽 버퍼 (DMA-BUF 대안)

GPU가 없거나 소프트웨어 렌더링을 사용하는 환경에서 memfd는 그래픽 버퍼로 활용됩니다. Wayland의 wl_shm, PipeWire의 memfd 기반 오디오/비디오 버퍼가 대표적입니다.

3. JIT 컴파일러

JavaScript V8, Java HotSpot, LuaJIT 등의 JIT 컴파일러는 memfd를 사용하여 동적 생성 코드를 안전하게 실행합니다. W^X(Write XOR Execute) 원칙을 준수하기 위해:

  1. memfd에 MFD_EXEC 플래그로 생성
  2. PROT_WRITE로 mmap하여 기계어(Machine Code) 코드 기록
  3. mprotect()PROT_EXEC로 전환 (또는 별도 매핑)
  4. 코드 실행
/* JIT: W^X 패턴 with memfd */
int fd = memfd_create("jit-code", MFD_CLOEXEC | MFD_EXEC);
ftruncate(fd, code_size);

/* 쓰기 전용 매핑 */
void *w = mmap(NULL, code_size, PROT_WRITE,
    MAP_SHARED, fd, 0);
memcpy(w, generated_code, code_size);
munmap(w, code_size);

/* 실행 전용 매핑 (다른 가상 주소) */
void *x = mmap(NULL, code_size, PROT_READ | PROT_EXEC,
    MAP_SHARED, fd, 0);

/* JIT 코드 실행 */
((void (*)())x)();

memfd 활용 프로젝트 예시

프로젝트용도사용 패턴
Wayland (wlroots, Mutter) wl_shm 그래픽 버퍼 memfd_create + mmap + fd 전달
systemd (sd-bus) D-Bus 대용량 메시지 memfd_create + seal + UNIX_FD
PipeWire 오디오/비디오 버퍼 memfd_create + mmap
QEMU/KVM 게스트 메모리 백엔드 memfd_create + MFD_HUGETLB
Firefox (IPC) 멀티프로세스 IPC memfd_create + seal + SCM_RIGHTS
Chromium 공유 메모리 리전 memfd_create + MFD_ALLOW_SEALING

memfd와 zswap/zram의 운영 경계

memfd와 zswap/zram은 모두 메모리 관리 문맥에서 자주 함께 언급되지만, 역할 계층이 다릅니다. memfd는 사용자 공간의 공유 버퍼를 표현하는 파일 디스크립터 API이고, zswap/zram은 메모리 압박 시 페이지를 압축해 보관하는 스왑(Swap) 계층입니다.

핵심 정리: memfd는 "데이터를 어떻게 공유할지"를 결정하고, zswap/zram은 "메모리가 부족할 때 해당 페이지를 어떻게 회수/보관할지"를 결정합니다. 즉, 둘은 대체 관계가 아니라 직교 관계입니다.
항목memfdzswapzram
역할 익명 파일 기반 공유 버퍼 API 스왑 아웃 페이지의 RAM 압축 캐시(Cache) 압축된 RAM 블록 디바이스
주요 인터페이스 memfd_create(), fcntl(F_ADD_SEALS), mmap() /sys/module/zswap/parameters/*, frontswap /dev/zramN, zramctl, swapon
활성 조건 애플리케이션이 명시적으로 사용 스왑 활성 + zswap 활성 + 메모리 압박 관리자가 zram 장치 구성 후 swapon
튜닝 주체 개발자(버퍼 크기, seal, mmap 정책) 운영자(압축기, 풀 크기, 임계치) 운영자(디바이스 크기, 알고리즘, 우선순위(Priority))
문서 위치 이 문서 (IPC/보안/API) zswap 문서 (내부 구조/디버깅(Debugging)) Swapping 문서 (운영/정책)
API 계층과 회수 계층은 분리해서 설계 사용자 공간 버퍼 계층 (memfd) 프로세스 A/B + SCM_RIGHTS memfd + mmap + sealing 메모리 압박 시 페이지 회수로 연결 스왑 회수 계층 (zswap / zram) zswap RAM 압축 캐시 swap 장치 SSD / HDD zram 압축 RAM 블록

관측과 디버깅 체크리스트

memfd 장애는 보통 "API 사용 오류"와 "메모리 압박에 따른 간접 영향"이 섞여 나타납니다. 아래 순서대로 보면 원인 분리가 빠릅니다.

  1. fd 존재 확인 -- /proc/<pid>/fd에서 memfd:name 링크를 확인
  2. 매핑 상태 확인 -- /proc/<pid>/mapssmaps에서 공유 매핑/권한 확인
  3. seal 확인 -- fcntl(fd, F_GET_SEALS) 값으로 불변성 정책 검증
  4. 스왑 영향 분리 -- 성능 저하 시 Swapping 문서의 지표로 zswap/zram 개입 여부 확인
# 1) 프로세스가 보유한 memfd 확인
ls -l /proc/$PID/fd | grep memfd

# 2) 매핑된 영역과 권한 확인
grep -n "memfd" /proc/$PID/maps
grep -n "memfd" /proc/$PID/smaps

# 3) fdinfo에서 inode/flags 확인
cat /proc/$PID/fdinfo/$FD

# 4) 전역 메모리 압박 확인 (간접 영향 분리)
cat /proc/meminfo | egrep 'MemAvailable|SwapTotal|SwapFree'
cat /proc/vmstat | egrep 'pswpin|pswpout'
/* seal 상태 점검 유틸리티 */
static void dump_memfd_seals(int fd)
{
    int seals = fcntl(fd, F_GET_SEALS);
    if (seals < 0) {
        perror("F_GET_SEALS");
        return;
    }
    printf("seals=0x%x\\n", seals);
    if (seals & F_SEAL_SEAL)         puts("  - F_SEAL_SEAL");
    if (seals & F_SEAL_SHRINK)       puts("  - F_SEAL_SHRINK");
    if (seals & F_SEAL_GROW)         puts("  - F_SEAL_GROW");
    if (seals & F_SEAL_WRITE)        puts("  - F_SEAL_WRITE");
    if (seals & F_SEAL_FUTURE_WRITE) puts("  - F_SEAL_FUTURE_WRITE");
}
memfd 이슈 원인 분리 흐름 증상: IPC 실패 / 지연 증가 API 사용 오류 확인 fd 전달, seal, 매핑 권한, close 시점 메모리 압박 영향 확인 swap in/out, reclaim, zswap/zram 지표 대응: 코드 수정 - F_GET_SEALS 검증 추가 - fd 수명 소유권 규약 정리 - mmap 권한(W^X) 정합성 확인 대응: 운영 튜닝 - swappiness, memory.high 점검 - zswap/zram 정책 재검토 - 워크로드 메모리 상한 재설계

자주 발생하는 장애 패턴

패턴원인증상대응
seal 누락 송신 측이 F_SEAL_WRITE를 적용하지 않음 수신 측 데이터 무결성(Integrity) 붕괴 전송 전 seal 강제, 수신 측 F_GET_SEALS 검증
fd 수명 경합(Contention) 한쪽 프로세스가 예상보다 빨리 close() 재시작(Reboot) 시 누락/EBADF 소유권 규약(생성자/소비자) 문서화, dup/참조 관리
실행 권한 과다 MFD_NOEXEC_SEAL 미사용 공격 표면 증가 기본값 noexec 정책, 필요 시에만 명시 실행 허용
대용량 버퍼 지연 4KB 페이지 다량 fault + NUMA 원격 접근 지연 편차 급증 HugeTLB, pre-fault, NUMA 바인딩 적용
운영 지표 혼동 memfd 문제와 swap 압박을 한 원인으로 취급 튜닝 반복에도 개선 미미 API 문제/운영 문제 분리 보고서 작성

실패 재현 코드: seal 검증 누락 사례

/* 수신 측에서 seal 검증을 누락하면 발생 가능한 실수 */
int recv_fd = recv_fd_over_unix_socket(sock);
void *p = mmap(NULL, size, PROT_READ, MAP_SHARED, recv_fd, 0);

/* 잘못된 가정: 송신 측이 이미 봉인했을 것이라고 믿음 */
if (((char *)p)[0] != expected_magic) {
    /* 런타임에서 가끔 실패: 경쟁 상태로 데이터 변경 가능 */
}

/* 올바른 패턴: 수신 측에서 직접 seal 확인 */
int seals = fcntl(recv_fd, F_GET_SEALS);
if (!(seals & F_SEAL_WRITE)) {
    fprintf(stderr, "untrusted memfd: writable\\n");
    abort();
}
수신 측 seal 검증을 반드시 포함 취약 패턴 1) fd 수신 후 즉시 mmap 2) seal 확인 생략 3) 경쟁 상태에서 데이터 변조 가능 결과: 간헐적 무결성 실패 권장 패턴 1) fd 수신 2) F_GET_SEALS로 정책 검증 3) 검증 통과 시 mmap + 파싱 결과: 재현 가능한 무결성 보장 검증 단계 추가 팀 운영 규칙 예시 - 송신 측: 전송 전에 seal 적용 로그 기록 - 수신 측: seal/size/version 불일치 시 즉시 폐기 - 장애 보고: API 계층 이슈와 swap 압박 이슈를 분리

HugeTLB memfd

MFD_HUGETLB 플래그로 생성된 memfd는 tmpfs 대신 hugetlbfs를 배후 저장소로 사용합니다. 2MB 또는 1GB 단위의 대형 페이지(HugePage)를 할당하여 TLB 미스(TLB Miss)를 크게 줄이고, 대용량 메모리 매핑의 성능을 향상시킵니다.

일반 memfd vs HugeTLB memfd 비교 일반 memfd (tmpfs) shmem_file_setup() → tmpfs inode 4KB 페이지 단위 할당 swap 가능 File Sealing 지원 16GB 매핑 시 TLB 엔트리 ~4M개 필요 CONFIG_TMPFS만 필요 HugeTLB memfd (hugetlbfs) hugetlb_file_setup() → hugetlbfs inode 2MB/1GB 페이지 단위 할당 swap 불가 (고정 할당) File Sealing 제한적 16GB 매핑 시 TLB 엔트리 ~8K개 (2MB 기준) CONFIG_HUGETLBFS + 사전 할당 필요
/* HugeTLB memfd 사용 예시: QEMU/KVM 게스트 메모리 백엔드 */
#include <sys/mman.h>

/* 2MB 페이지 기반 HugeTLB memfd 생성 */
int fd = memfd_create("guest-ram",
    MFD_CLOEXEC | MFD_HUGETLB | MFD_HUGE_2MB);
if (fd < 0) {
    perror("memfd_create(HUGETLB)");
    /* 실패 원인: hugepages 미할당 또는 권한 부족 */
}

/* 크기는 2MB 배수로 설정 (그렇지 않으면 올림됨) */
size_t guest_mem_size = 4UL * 1024 * 1024 * 1024; /* 4GB */
ftruncate(fd, guest_mem_size);

/* 2MB 페이지 매핑 (MAP_HUGETLB 플래그는 불필요 - fd가 이미 hugetlbfs) */
void *guest_mem = mmap(NULL, guest_mem_size,
    PROT_READ | PROT_WRITE,
    MAP_SHARED | MAP_POPULATE,  /* MAP_POPULATE: pre-fault */
    fd, 0);

/* 1GB 페이지 기반 (x86_64에서 지원) */
int fd_1g = memfd_create("huge-1g",
    MFD_CLOEXEC | MFD_HUGETLB | MFD_HUGE_1GB);
# HugeTLB 사전 할당 (부팅 시 또는 런타임)
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 2MB * 1024 = 2GB의 hugepages 사전 할당

# 현재 hugepage 상태 확인
cat /proc/meminfo | grep -i huge
# HugePages_Total:   1024
# HugePages_Free:     512
# HugePages_Rsvd:     256
# HugePages_Surp:       0
# Hugepagesize:      2048 kB

# 특정 NUMA 노드에 hugepage 할당
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
QEMU/KVM 최적화: QEMU는 -mem-path 대신 -object memory-backend-memfd,id=mem0,size=4G,hugetlb=on,hugetlbsize=2M 옵션으로 HugeTLB memfd를 게스트 메모리 백엔드로 사용할 수 있습니다. 이 방식은 파일 시스템 경로가 불필요하고, VFIO 디바이스 패스스루 시 IOMMU 매핑이 단순해지는 이점이 있습니다.

ftrace / bpftrace를 이용한 memfd 추적

memfd 관련 성능 문제나 보안 감사(Audit)를 위해 커널 트레이싱 도구를 활용할 수 있습니다. ftrace의 kprobe, bpftrace, perf를 사용한 추적 방법을 소개합니다.

ftrace: memfd_create 시스콜 추적

# ftrace로 memfd_create 호출 추적
cd /sys/kernel/debug/tracing

# sys_memfd_create 함수 kprobe 설정
echo 'p:memfd_probe __x64_sys_memfd_create flags=%si:u32' > kprobe_events
echo 1 > events/kprobes/memfd_probe/enable
echo 1 > tracing_on

# 추적 로그 확인
cat trace_pipe
# python3-1234 [002] .... 123.456: memfd_probe: (__x64_sys_memfd_create+0x0/0x1f0) flags=0x3
# → flags=0x3 = MFD_CLOEXEC(0x1) | MFD_ALLOW_SEALING(0x2)

# 정리
echo 0 > tracing_on
echo '-:memfd_probe' > kprobe_events

bpftrace: memfd 생성과 seal 실시간 모니터링

#!/usr/bin/env bpftrace
# memfd_create 호출과 flags 모니터링

tracepoint:syscalls:sys_enter_memfd_create
{
    printf("%s[%d] memfd_create(name=%s, flags=0x%x",
        comm, pid,
        str(args->uname),
        args->flags);

    /* 플래그 해석 */
    if (args->flags & 0x01) { printf(" CLOEXEC"); }
    if (args->flags & 0x02) { printf(" ALLOW_SEALING"); }
    if (args->flags & 0x04) { printf(" HUGETLB"); }
    if (args->flags & 0x08) { printf(" NOEXEC_SEAL"); }
    if (args->flags & 0x10) { printf(" EXEC"); }
    printf(")\n");
}

tracepoint:syscalls:sys_exit_memfd_create
{
    printf("  → fd=%d\n", args->ret);
}

/* F_ADD_SEALS 추적 */
tracepoint:syscalls:sys_enter_fcntl
/args->cmd == 1033/  /* F_ADD_SEALS = 1033 */
{
    printf("%s[%d] fcntl(fd=%d, F_ADD_SEALS, seals=0x%lx)\n",
        comm, pid, args->fd, args->arg);
}

perf: memfd 페이지 폴트 성능 분석

# memfd 관련 페이지 폴트 이벤트 수집
perf stat -e 'page-faults,minor-faults,major-faults' \
    -p $PID -- sleep 10

# shmem_fault 호출 스택 프로파일링
perf record -g -e 'probe:shmem_fault' -p $PID -- sleep 10
perf report --stdio

# shmem_fault kprobe 등록
perf probe --add 'shmem_fault'

# memfd 관련 시스콜 지연 분석
perf trace -e 'memfd_create,fcntl,mmap,ftruncate' -p $PID
보안 감사 팁: 프로덕션 환경에서 MFD_EXEC 플래그를 사용하는 memfd 생성을 모니터링하려면, 위의 bpftrace 스크립트에서 args->flags & 0x10 조건에 알림(Alert)을 추가하세요. 이는 잠재적인 파일리스 코드 실행 시도를 탐지하는 데 유용합니다. auditd로도 -S memfd_create 규칙을 추가할 수 있습니다.
# auditd 규칙: memfd_create 시스콜 감사
auditctl -a always,exit -F arch=b64 -S memfd_create -k memfd_audit

# MFD_EXEC 사용 필터링
ausearch -k memfd_audit | grep 'a1=.*10'
# a1은 두 번째 인자(flags), 0x10 = MFD_EXEC

흔한 실수 모음

#실수증상올바른 방법
1 MFD_ALLOW_SEALING 누락 후 seal 시도 fcntl(F_ADD_SEALS)-EPERM 생성 시 반드시 MFD_ALLOW_SEALING 포함
2 쓰기 mmap 해제 전 F_SEAL_WRITE 적용 fcntl(F_ADD_SEALS)-EBUSY munmap() 후 seal 적용, 또는 F_SEAL_FUTURE_WRITE 사용
3 ftruncate() 호출 누락 mmap 시 SIGBUS (크기 0인 파일에 접근) mmap 전에 반드시 ftruncate(fd, size) 호출
4 수신 측에서 seal 검증 생략 TOCTOU 경쟁 상태로 데이터 변조 가능 수신 즉시 fcntl(F_GET_SEALS)로 검증
5 memfd_secret에 read()/write() 시도 -ENOSYS 또는 빈 데이터 반환 memfd_secret은 mmap()으로만 접근 가능
6 HugeTLB memfd 크기를 hugepage 배수 아닌 값으로 설정 자동 올림(Round-Up)으로 예상 외 메모리 사용 크기를 hugepage 크기(2MB/1GB)의 배수로 명시 설정
7 fork 후 양쪽에서 동시에 seal 적용 예상치 못한 seal 순서, 경합 상태 seal 적용 주체를 한쪽(생성자)으로 한정, 소비자는 검증만
8 MFD_NOEXEC_SEAL 미사용 memfd로 파일리스 코드 실행 가능 (보안 위험) JIT 외에는 항상 MFD_NOEXEC_SEAL 기본 사용
9 memfd fd를 close하지 않고 프로세스 종료 의존 fd를 전달받은 프로세스가 살아있으면 메모리 미해제 사용 후 명시적 close(fd), fd 수명 소유권 문서화
10 SCM_RIGHTS 전송 시 iov_len = 0 일부 커널/libc 조합에서 sendmsg() 실패 최소 1바이트 데이터(iov_len = 1)를 함께 전송

실습 가이드

실습 1: memfd + seal + SCM_RIGHTS 기본 흐름

목표: memfd를 생성하고, 데이터를 쓰고, seal을 적용한 뒤, Unix 도메인 소켓으로 다른 프로세스에 전달하는 전체 흐름을 체험합니다.
/* lab1_memfd_ipc.c — 컴파일: gcc -o lab1 lab1_memfd_ipc.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <fcntl.h>

static void send_fd(int sock, int fd) {
    struct msghdr msg = {0};
    char buf[CMSG_SPACE(sizeof(int))];
    struct iovec iov = { .iov_base = "x", .iov_len = 1 };
    msg.msg_iov = &iov; msg.msg_iovlen = 1;
    msg.msg_control = buf; msg.msg_controllen = sizeof(buf);
    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
    sendmsg(sock, &msg, 0);
}

static int recv_fd(int sock) {
    struct msghdr msg = {0};
    char buf[CMSG_SPACE(sizeof(int))], dummy;
    struct iovec iov = { .iov_base = &dummy, .iov_len = 1 };
    msg.msg_iov = &iov; msg.msg_iovlen = 1;
    msg.msg_control = buf; msg.msg_controllen = sizeof(buf);
    recvmsg(sock, &msg, 0);
    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
    int fd; memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
    return fd;
}

int main(void) {
    int sv[2];
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    if (fork() == 0) {
        /* === 자식: 소비자 === */
        close(sv[0]);
        int fd = recv_fd(sv[1]);

        /* seal 검증 */
        int seals = fcntl(fd, F_GET_SEALS);
        printf("[소비자] seals=0x%x\n", seals);
        if (!(seals & F_SEAL_WRITE)) {
            fprintf(stderr, "경고: 쓰기 봉인 안 됨!\n");
            return 1;
        }

        /* 안전하게 읽기 */
        struct stat st;
        fstat(fd, &st);
        char *p = mmap(NULL, st.st_size,
            PROT_READ, MAP_SHARED, fd, 0);
        printf("[소비자] 수신: %.*s\n",
            (int)st.st_size, p);

        munmap(p, st.st_size);
        close(fd);
        return 0;
    }

    /* === 부모: 생산자 === */
    close(sv[1]);

    /* 1. memfd 생성 */
    int fd = memfd_create("lab1",
        MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL);
    ftruncate(fd, 4096);

    /* 2. 데이터 기록 */
    const char *msg_data = "Hello from memfd!";
    write(fd, msg_data, strlen(msg_data));

    /* 3. seal 적용 */
    fcntl(fd, F_ADD_SEALS,
        F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);
    printf("[생산자] seal 적용 완료\n");

    /* 4. fd 전달 */
    send_fd(sv[0], fd);
    close(fd);

    wait(NULL);
    return 0;
}
# 컴파일 및 실행
gcc -o lab1 lab1_memfd_ipc.c
./lab1
# 출력:
# [생산자] seal 적용 완료
# [소비자] seals=0x2e
# [소비자] 수신: Hello from memfd!

실습 2: memfd_secret으로 비밀 키 보호

/* lab2_memfd_secret.c — 컴파일: gcc -o lab2 lab2_memfd_secret.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>

int main(void) {
    /* memfd_secret 생성 */
    int fd = syscall(SYS_memfd_secret, 0);
    if (fd < 0) {
        perror("memfd_secret (커널 지원 또는 secretmem.enable 확인)");
        return 1;
    }
    printf("memfd_secret fd=%d\n", fd);

    ftruncate(fd, 4096);

    /* mmap으로만 접근 가능 */
    char *secret = mmap(NULL, 4096,
        PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (secret == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    /* 비밀 데이터 저장 */
    strcpy(secret, "AES-256-KEY:abcdef1234567890");
    printf("비밀 저장 완료. /proc/%d/maps를 확인해 보세요.\n", getpid());
    printf("sudo cat /proc/%d/mem 으로는 읽을 수 없습니다.\n", getpid());

    /* read() 시스콜로는 접근 불가 확인 */
    char buf[32];
    lseek(fd, 0, SEEK_SET);
    ssize_t n = read(fd, buf, sizeof(buf));
    printf("read() 결과: %zd (0 또는 오류 예상)\n", n);

    printf("Enter를 누르면 종료...\n");
    getchar();

    /* 명시적 제로화 후 해제 */
    explicit_bzero(secret, 4096);
    munmap(secret, 4096);
    close(fd);
    return 0;
}

실습 3: seal 충돌 디버깅

/* lab3_seal_debug.c — seal 적용 시 발생하는 다양한 오류 체험 */
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(void) {
    /* 테스트 1: MFD_ALLOW_SEALING 없이 seal 시도 */
    int fd1 = memfd_create("no-seal", MFD_CLOEXEC);
    int ret = fcntl(fd1, F_ADD_SEALS, F_SEAL_WRITE);
    printf("테스트1 (ALLOW_SEALING 없음): ret=%d errno=%s\n",
        ret, strerror(errno));
    close(fd1);

    /* 테스트 2: 쓰기 mmap 존재 시 F_SEAL_WRITE */
    int fd2 = memfd_create("busy",
        MFD_CLOEXEC | MFD_ALLOW_SEALING);
    ftruncate(fd2, 4096);
    void *p = mmap(NULL, 4096,
        PROT_READ|PROT_WRITE, MAP_SHARED, fd2, 0);
    ret = fcntl(fd2, F_ADD_SEALS, F_SEAL_WRITE);
    printf("테스트2 (WRITE mmap 존재): ret=%d errno=%s\n",
        ret, strerror(errno));

    /* 해결: F_SEAL_FUTURE_WRITE 사용 */
    ret = fcntl(fd2, F_ADD_SEALS, F_SEAL_FUTURE_WRITE);
    printf("테스트2b (FUTURE_WRITE): ret=%d\n", ret);
    munmap(p, 4096);
    close(fd2);

    /* 테스트 3: F_SEAL_SEAL 이후 추가 seal */
    int fd3 = memfd_create("sealed",
        MFD_CLOEXEC | MFD_ALLOW_SEALING);
    fcntl(fd3, F_ADD_SEALS, F_SEAL_SEAL);
    ret = fcntl(fd3, F_ADD_SEALS, F_SEAL_WRITE);
    printf("테스트3 (SEAL 이후 추가): ret=%d errno=%s\n",
        ret, strerror(errno));
    close(fd3);

    /* 테스트 4: seal 적용 후 write 시도 */
    int fd4 = memfd_create("readonly",
        MFD_CLOEXEC | MFD_ALLOW_SEALING);
    ftruncate(fd4, 4096);
    fcntl(fd4, F_ADD_SEALS, F_SEAL_WRITE);
    ssize_t n = write(fd4, "fail", 4);
    printf("테스트4 (seal 후 write): n=%zd errno=%s\n",
        n, strerror(errno));
    close(fd4);

    return 0;
}
# 예상 출력:
# 테스트1 (ALLOW_SEALING 없음): ret=-1 errno=Operation not permitted
# 테스트2 (WRITE mmap 존재): ret=-1 errno=Device or resource busy
# 테스트2b (FUTURE_WRITE): ret=0
# 테스트3 (SEAL 이후 추가): ret=-1 errno=Operation not permitted
# 테스트4 (seal 후 write): n=-1 errno=Operation not permitted

참고자료

다음 학습:
  • VMA / mmap - memfd와 함께 사용되는 가상 메모리 매핑의 내부 구현
  • 메모리 관리 개요 - tmpfs/shmem의 배후 메모리 관리
  • IPC - 프로세스 간 통신의 다양한 메커니즘 비교
  • LSM / Seccomp - memfd 접근 제어(Access Control) 및 보안 정책