memfd (Memory File Descriptors)
memfd_create()는 파일 시스템에 존재하지 않는 익명 메모리 파일을 생성하는 리눅스 시스콜입니다.
tmpfs 기반의 이 메모리 파일은 mmap(), read()/write(),
ftruncate() 등 일반 파일 연산을 모두 지원하면서도 디스크에 흔적을 남기지 않습니다.
File Sealing을 통해 공유 메모리의 불변성을 보장하고, SCM_RIGHTS를 통해 프로세스(Process) 간
안전한 메모리 교환이 가능합니다. 이 문서에서는 커널 내부 구현부터 보안 고려사항,
Wayland/D-Bus 활용, memfd_secret()까지 전 영역을 다룹니다.
핵심 요약
- 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+)
단계별 이해
- 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()의 커널 내부 호출 흐름은 다음과 같습니다.
사용자 공간(User Space)에서 시스콜을 호출하면, 커널은 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매크로(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() 함수 구현 분석
커널 소스 mm/memfd.c의 memfd_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_noexecsysctl 값(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_SEAL은FMODE_EXEC를 제거하고F_SEAL_EXEC를 적용하여 실행 권한을 영구적으로 차단합니다.MFD_EXEC는 반대로 실행을 명시적으로 허용합니다.
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,
};
shmem_fault(): 페이지 폴트 경로
memfd의 mmap 매핑에 처음 접근하면 페이지 폴트(Page Fault)가 발생합니다.
이때 커널의 shmem_fault() 함수가 호출되어 실제 물리 페이지를 할당합니다.
이 지연 할당(Lazy Allocation) 방식 덕분에 ftruncate()로 큰 크기를 설정해도
즉시 메모리가 소비되지 않습니다.
/* 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 엔트리 관리가 적용됩니다.
/* 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가 속한 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_CLOEXEC는get_unused_fd_flags()에O_CLOEXEC를 전달하여,exec()시 자동으로 fd가 닫히도록 합니다. -
3행
MFD_ALLOW_SEALING은shmem_inode_info.seals에서 기본F_SEAL_SEAL을 제거합니다. 이 seal이 제거되어야fcntl(F_ADD_SEALS)가 작동합니다. -
4행
MFD_HUGETLB는 tmpfs 대신 hugetlbfs를 사용하여 대용량 페이지(HugePage) 기반 메모리를 할당합니다. -
5-6행
MFD_NOEXEC_SEAL은file->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 file의 f_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_GROWseal을 확인합니다. -
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은 이러한 변경을 커널 수준에서 영구적으로 금지합니다.
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 필드에 적용하는 핵심 로직입니다.
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_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를 전달하는 방법은 크게 세 가지입니다:
- 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() 시스콜 오버헤드(Overhead) 없이 메모리를 직접 접근할 수 있으며,
여러 프로세스가 동일한 물리 페이지를 공유합니다.
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에 도입된 시스콜로, 커널조차 접근할 수 없는
비밀 메모리 영역을 생성합니다. 암호화(Encryption) 키, 비밀번호 등 민감한 데이터를 보호하는 데 사용됩니다.
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 플러시(Flush) 비용이 발생하며,
hibernation을 비활성화할 수 있습니다. 성능이 중요한 대량 할당에는 부적합합니다.
memfd_secret 커널 내부 구현
memfd_secret()의 핵심 보안 기능은 mm/secretmem.c에 구현되어 있습니다.
일반 memfd와 달리 자체 file_operations를 사용하며, 페이지 폴트 시
set_direct_map_invalid_noflush()로 커널의 direct map에서 해당 페이지를 제거합니다.
/* 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_create의 MFD_EXEC 플래그를 차단할 수도 있습니다.
컨테이너/Seccomp 환경에서의 memfd 보안
컨테이너(Container) 환경에서 memfd는 특별한 보안 고려가 필요합니다. 기본적으로 대부분의
컨테이너 런타임(Docker, containerd, CRI-O)은 memfd_create()를 허용하므로,
컨테이너 내부에서 파일리스 공격이 가능합니다.
// 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);
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 가능합니다.
성능 최적화 팁
| 최적화 | 방법 | 효과 |
|---|---|---|
| 대용량 버퍼 | 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) 원칙을 준수하기 위해:
- memfd에
MFD_EXEC플래그로 생성 PROT_WRITE로 mmap하여 기계어(Machine Code) 코드 기록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은 메모리 압박 시 페이지를 압축해 보관하는 스왑(Swap) 계층입니다.
| 항목 | memfd | zswap | zram |
|---|---|---|---|
| 역할 | 익명 파일 기반 공유 버퍼 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 문서 (운영/정책) |
관측과 디버깅 체크리스트
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를 적용하지 않음 |
수신 측 데이터 무결성(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();
}
HugeTLB memfd
MFD_HUGETLB 플래그로 생성된 memfd는 tmpfs 대신 hugetlbfs를
배후 저장소로 사용합니다. 2MB 또는 1GB 단위의 대형 페이지(HugePage)를 할당하여
TLB 미스(TLB Miss)를 크게 줄이고, 대용량 메모리 매핑의 성능을 향상시킵니다.
/* 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
-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 기본 흐름
/* 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
참고자료
- 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
- LWN: memfd and file sealing (2014) — memfd_create와 파일 실링 설계 논의
- mm/memfd.c (Bootlin) — memfd 핵심 구현 소스
- mm/secretmem.c (Bootlin) — memfd_secret 구현 소스
- Kernel Documentation: memfd_secret — memfd_secret 사용자 공간 API 문서
- VMA / mmap - memfd와 함께 사용되는 가상 메모리 매핑의 내부 구현
- 메모리 관리 개요 - tmpfs/shmem의 배후 메모리 관리
- IPC - 프로세스 간 통신의 다양한 메커니즘 비교
- LSM / Seccomp - memfd 접근 제어(Access Control) 및 보안 정책