memfd (Memory File Descriptors) 심화

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

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

핵심 요약

  • memfd_create() — 파일 시스템에 존재하지 않는 익명 메모리 파일을 생성하는 시스콜 (Linux 3.17+)
  • File Sealing — 파일 내용/크기의 변경을 영구적으로 금지하는 메커니즘 (fcntl(F_ADD_SEALS))
  • SCM_RIGHTS — Unix 도메인 소켓을 통해 파일 디스크립터를 다른 프로세스에 전달하는 방법
  • 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()의 커널 내부 호출 흐름은 다음과 같습니다. 사용자 공간에서 시스콜을 호출하면, 커널은 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 매크로로 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 내부 구현 (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,
};

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를 사용합니다.

File Sealing 메커니즘

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() 매핑이 없어야 합니다. 존재하면 -EBUSY가 반환됩니다. F_SEAL_FUTURE_WRITE(Linux 5.1+)는 기존 매핑은 유지하면서 새로운 쓰기 매핑만 차단합니다.

memfd와 프로세스 간 공유

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() 시스콜 오버헤드 없이 메모리를 직접 접근할 수 있으며, 여러 프로세스가 동일한 물리 페이지를 공유합니다.

프로세스 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에 도입된 시스콜로, 커널조차 접근할 수 없는 비밀 메모리 영역을 생성합니다. 암호화 키, 비밀번호 등 민감한 데이터를 보호하는 데 사용됩니다.

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 플러시 비용이 발생하며, hibernation을 비활성화할 수 있습니다. 성능이 중요한 대량 할당에는 부적합합니다.

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를 권장합니다. 컨테이너 환경에서는 seccomp 프로필로 memfd_createMFD_EXEC 플래그를 차단할 수도 있습니다.

커널 설정과 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 미스 감소, 페이지 폴트 횟수 감소
페이지 사전 할당 fallocate(fd, 0, 0, size) 첫 접근 시 페이지 폴트 방지
NUMA 인지 mbind() / set_mempolicy() NUMA 노드 간 접근 지연 감소
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하여 기계어 코드 기록
  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은 메모리 압박 시 페이지를 압축해 보관하는 스왑 계층입니다.

핵심 정리: memfd는 "데이터를 어떻게 공유할지"를 결정하고, zswap/zram은 "메모리가 부족할 때 해당 페이지를 어떻게 회수/보관할지"를 결정합니다. 즉, 둘은 대체 관계가 아니라 직교 관계입니다.
항목memfdzswapzram
역할 익명 파일 기반 공유 버퍼 API 스왑 아웃 페이지의 RAM 압축 캐시 압축된 RAM 블록 디바이스
주요 인터페이스 memfd_create(), fcntl(F_ADD_SEALS), mmap() /sys/module/zswap/parameters/*, frontswap /dev/zramN, zramctl, swapon
활성 조건 애플리케이션이 명시적으로 사용 스왑 활성 + zswap 활성 + 메모리 압박 관리자가 zram 장치 구성 후 swapon
튜닝 주체 개발자(버퍼 크기, seal, mmap 정책) 운영자(압축기, 풀 크기, 임계치) 운영자(디바이스 크기, 알고리즘, 우선순위)
문서 위치 이 문서 (IPC/보안/API) zswap 문서 (내부 구조/디버깅) 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를 적용하지 않음 수신 측 데이터 무결성 붕괴 전송 전 seal 강제, 수신 측 F_GET_SEALS 검증
fd 수명 경합 한쪽 프로세스가 예상보다 빨리 close() 재시작 시 누락/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 압박 이슈를 분리

참고자료

다음 학습: