memfd (Memory File Descriptors) 심화
memfd_create()는 파일 시스템에 존재하지 않는 익명 메모리 파일을 생성하는 리눅스 시스콜입니다.
tmpfs 기반의 이 메모리 파일은 mmap(), read()/write(),
ftruncate() 등 일반 파일 연산을 모두 지원하면서도 디스크에 흔적을 남기지 않습니다.
File Sealing을 통해 공유 메모리의 불변성을 보장하고, SCM_RIGHTS를 통해 프로세스 간
안전한 메모리 교환이 가능합니다. 이 문서에서는 커널 내부 구현부터 보안 고려사항,
Wayland/D-Bus 활용, memfd_secret()까지 전 영역을 다룹니다.
핵심 요약
- 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+)
단계별 이해
- memfd 생성
memfd_create("name", flags)로 익명 파일 디스크립터를 얻습니다. 이 파일은 어떤 디렉터리에도 존재하지 않습니다. - 크기 설정
ftruncate(fd, size)로 메모리 파일의 크기를 지정합니다. tmpfs가 배후에서 페이지를 관리합니다. - 데이터 읽기/쓰기
write()/read()또는mmap()으로 데이터를 조작합니다. - 봉인(Seal) 적용
fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK)로 쓰기/축소를 영구 금지합니다. - 프로세스 간 공유
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을 할당한 뒤 파일 디스크립터 번호를 반환합니다.
커널 소스: __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의 핵심 특성입니다.
핵심 커널 자료구조 관계
/* 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은 이러한 변경을 커널 수준에서 영구적으로 금지합니다.
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를 전달하는 방법은 크게 세 가지입니다:
- SCM_RIGHTS: Unix 도메인 소켓의 ancillary data로 fd 전달 (가장 일반적)
- pidfd_getfd(): 대상 프로세스의 fd를 직접 복제 (Linux 5.6+)
- fork(): 자식 프로세스가 부모의 fd를 상속
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() 시스콜 오버헤드 없이 메모리를 직접 접근할 수 있으며,
여러 프로세스가 동일한 물리 페이지를 공유합니다.
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하여 안전하게 접근 */
read()/write()
시스콜의 사용자-커널 복사 오버헤드가 사라집니다. 특히 대용량 데이터 전송에서
pipe()나 소켓 기반 IPC보다 월등한 성능을 보입니다.
memfd_secret (비밀 메모리)
memfd_secret()는 Linux 5.14에 도입된 시스콜로, 커널조차 접근할 수 없는
비밀 메모리 영역을 생성합니다. 암호화 키, 비밀번호 등 민감한 데이터를 보호하는 데 사용됩니다.
memfd_secret vs memfd_create 차이
| 특성 | memfd_create | memfd_secret |
|---|---|---|
| 배후 저장소 | tmpfs (shmem) | secretmem (전용) |
| direct map 제거 | 아니오 | 예 (핵심!) |
| 커널 접근 | 가능 (kmap 등) | 불가능 |
| /proc/kcore 노출 | 가능 | 불가능 |
| hibernation 시 디스크 기록 | 가능 | 불가능 |
| 다른 프로세스 공유 | 가능 (SCM_RIGHTS) | 불가능 |
| File Sealing | 지원 | 미지원 |
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_create의 MFD_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 가능합니다.
성능 최적화 팁
| 최적화 | 방법 | 효과 |
|---|---|---|
| 대용량 버퍼 | 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) 원칙을 준수하기 위해:
- memfd에
MFD_EXEC플래그로 생성 PROT_WRITE로 mmap하여 기계어 코드 기록mprotect()로PROT_EXEC로 전환 (또는 별도 매핑)- 코드 실행
/* 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 |
|---|---|---|---|
| 역할 | 익명 파일 기반 공유 버퍼 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 문서 (운영/정책) |
관측과 디버깅 체크리스트
memfd 장애는 보통 "API 사용 오류"와 "메모리 압박에 따른 간접 영향"이 섞여 나타납니다. 아래 순서대로 보면 원인 분리가 빠릅니다.
- fd 존재 확인 --
/proc/<pid>/fd에서memfd:name링크를 확인 - 매핑 상태 확인 --
/proc/<pid>/maps와smaps에서 공유 매핑/권한 확인 - seal 확인 --
fcntl(fd, F_GET_SEALS)값으로 불변성 정책 검증 - 스왑 영향 분리 -- 성능 저하 시 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");
}
자주 발생하는 장애 패턴
| 패턴 | 원인 | 증상 | 대응 |
|---|---|---|---|
| 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();
}
참고자료
- memfd_create(2) - Linux man page
- memfd_secret(2) - Linux man page
- Linux Kernel Documentation: Shared Memory
- LWN.net: memfd and file sealing (2014)
- LWN.net: memfd_secret() in 5.14 (2021)
- LWN.net: Restricting memfd_create() with MFD_NOEXEC_SEAL (2023)
- 커널 소스: mm/memfd.c
- 커널 소스: mm/secretmem.c
- 커널 소스: mm/shmem.c
- VMA / mmap 심화 - memfd와 함께 사용되는 가상 메모리 매핑의 내부 구현
- 메모리 관리 개요 (심화) - tmpfs/shmem의 배후 메모리 관리
- IPC - 프로세스 간 통신의 다양한 메커니즘 비교
- LSM / Seccomp 심화 - memfd 접근 제어 및 보안 정책