Secret Memory는 Linux 커널에서 _userspace만 접근 가능한 메모리 영역_을 생성하는 메커니즘이다. memfd_secret(2) 시스템 호출을 통해 생성된 파일 디스크립터를 mmap하면, 해당 메모리는 커널의 직접 매핑(direct map)에서 제외된다. 즉, 커널이 페이지 테이블에서 해당 페이지를 제거하여 자체적으로도 읽을 수 없게 된다. 이는 비밀키 저장소, 암호화 키 관리,TEE(Trusted Execution Environment) 런타임과 같은 보안이 중요한 사용 사례에 활용된다.
핵심 원리는 간단하다: 페이지를 할당한 뒤 set_direct_map_invalid_noflush()를 호출하여 커널의 linear map에서 해당 페이지를 unmap하고, TLB 플러시를 통해 실제 매핑을 무효화한다. 이후 사용자 공간만이 페이지 테이블 엔트리를 통해 해당 페이지에 접근할 수 있다. GUP(Get User Pages) 경로에서도 secretmem 페이지는 명시적으로 거부되어 커널이 우회할 수 없도록 차단된다.
소스 파일 경로:
mm/secretmem.c ← 메인 구현 (270줄)
include/linux/secretmem.h ← 헤더 (36줄)
mm/gup.c ← GUP-fast secretmem 거부 로직
mm/mlock.c ← VM_LOCKED 플래그 처리
arch/x86/mm/pat/set_memory.c ← set_direct_map_invalid_noflush 구현
fs/anon_inodes.c ← anon_inode_make_secure_inode
# 1. secretmem 활성화 여부 확인 (커널 빌드 옵션)
cat /boot/config-$(uname -r) | grep SECRETMEM
# 2. memfd_secret 시스템 호출 존재 여부 확인
grep -c memfd_secret /usr/include/asm/unistd_64.h
# 3. 현재 secretmem 사용자 수 (활성 파일 디스크립터)
sudo cat /proc/sys/kernel/secretmem_users 2>/dev/null || echo "sysctl 미지원"
# 4. 커널 모듈/파라미터 확인
cat /sys/module/secretmem/parameters/enable 2>/dev/null || echo "모듈 미로드"
# 5. /proc/meminfo에서 secretmem 관련 확인
grep -i secret /proc/meminfo 2>/dev/null || echo "secretmem 전용 항목 없음"
# 6. 실제 테스트 프로그램 컴파일 및 실행
cat > /tmp/test_secretmem.c << 'EOF'
#include <stdio.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = syscall(SYS_memfd_secret, O_CLOEXEC);
if (fd < 0) { perror("memfd_secret"); return 1; }
void *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { perror("mmap"); return 1; }
/* 사용자 공간에서만 접근 가능 — 커널 직접 매핑에서 제외됨 */
printf("secretmem 페이지 할당 성공: %p\n", p);
munmap(p, 4096);
close(fd);
return 0;
}
EOF
gcc /tmp/test_secretmem.c -o /tmp/test_secretmem && /tmp/test_secretmem
# 7. ARM64 등 아키텍처별 direct map 제한 확인
zgrep CONFIG_ARCH_HAS_SET_DIRECT_MAP /proc/config.gz 2>/dev/null || echo "config 미지원"
// mm/secretmem.c:111-113
static const struct vm_operations_struct secretmem_vm_ops = {
.fault = secretmem_fault, /* 페이지 폴트 발생 시 호출되는 핸들러 */
};
secretmem_fault()를 호출한다.vma->vm_ops == &secretmem_vm_ops로 secretmem VMA 여부를 판단한다.// mm/secretmem.c:141-144
static const struct file_operations secretmem_fops = {
.release = secretmem_release, /* fd close 시 사용자 수 감소 */
.mmap_prepare = secretmem_mmap_prepare, /* mmap 준비 단계에서 VMA 설정 */
};
mmap_prepare은 Linux 7.0에서 도입된 새로운 mmap 경로로, VMA 생성 전에 검증과 설정을 수행한다.// mm/secretmem.c:158-162
const struct address_space_operations secretmem_aops = {
.dirty_folio = noop_dirty_folio, /* 더티 트래킹 불필요 */
.free_folio = secretmem_free_folio, /* 해제 시 direct map 복원 + 제로화 */
.migrate_folio = secretmem_migrate_folio, /* 마이그레이션 거부 (-EBUSY) */
};
migrate_folio가 -EBUSY를 반환하는 이유: secretmem 페이지는 커널이 직접 접근할 수 없으므로 마이그레이션이 불가능하다.free_folio에서 set_direct_map_default_noflush()로 direct map을 복원한 뒤 folio_zero_segment()로 내용을 제로화한다.// mm/secretmem.c:253-257
static struct file_system_type secretmem_fs = {
.name = "secretmem",
.init_fs_context = secretmem_init_fs_context,
.kill_sb = kill_anon_super,
};
memfd_secret이 사용하는 가상 파일 시스템이다.SB_I_NOEXEC | SB_I_NODEV 플래그로 실행/장치 접근을 차단한다.// mm/secretmem.c:38-41
static bool secretmem_enable __ro_after_init = 1;
module_param_named(enable, secretmem_enable, bool, 0400);
MODULE_PARM_DESC(secretmem_enable,
"Enable secretmem and memfd_secret(2) system call");
__ro_after_init으로 초기화 후 읽기 전용이 되어 런타임 변경을 방지한다.CONFIG_SECRETMEM=n으로 완전히 비활성화할 수도 있다.// mm/secretmem.c:43
static atomic_t secretmem_users;
secretmem_active()로 커널 전체에서 secretmem이 사용 중인지 확인할 수 있다.memfd_secret 시스템 호출에서 atomic_read(&secretmem_users) < 0 검사로 오버플로우를 방지한다.// mm/secretmem.c:50-109
static vm_fault_t secretmem_fault(struct vm_fault *vmf)
역할: secretmem VMA에서 페이지 폴트가 발생했을 때, 새 folio를 할당하고 커널 direct map에서 제거한다.
// mm/secretmem.c:61-100
if (((loff_t)vmf->pgoff << PAGE_SHIFT) >= i_size_read(inode))
return vmf_error(-EINVAL);
filemap_invalidate_lock_shared(mapping);
retry:
folio = filemap_lock_folio(mapping, offset);
if (IS_ERR(folio)) {
folio = folio_alloc(gfp | __GFP_ZERO, 0);
if (!folio) {
ret = VM_FAULT_OOM;
goto out;
}
err = set_direct_map_invalid_noflush(folio_page(folio, 0));
if (err) {
folio_put(folio);
ret = vmf_error(err);
goto out;
}
__folio_mark_uptodate(folio);
err = filemap_add_folio(mapping, folio, offset, gfp);
if (unlikely(err)) {
/* 큰 페이지 분할이 필요했다면, 무효화 표시 시점에 이미 수행됨 */
set_direct_map_default_noflush(folio_page(folio, 0));
folio_put(folio);
if (err == -EEXIST)
goto retry;
ret = vmf_error(err);
goto out;
}
addr = (unsigned long)folio_address(folio);
flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
}
흐름:
1. 파일 범위 초과 검사 (i_size_read)
2. filemap_lock_folio()로 기존 folio 탐색
3. 없으면 folio_alloc(gfp | __GFP_ZERO, 0)로 새 folio 할당
4. set_direct_map_invalid_noflush()로 커널 직접 매핑 제거
5. filemap_add_folio()로 매핑에 추가
6. flush_tlb_kernel_range()로 TLB 무효화
7. 기존 folio가 있으면 그대로 반환 (VM_FAULT_LOCKED)
반복 로직: filemap_add_folio()가 -EEXIST를 반환하면 goto retry로 재시도한다.
// mm/secretmem.c:121-134
static int secretmem_mmap_prepare(struct vm_area_desc *desc)
역할: mmap 시 VMA 플래그를 설정하고 secretmem 전용 vm_ops를 연결한다.
// mm/secretmem.c:123-133
const unsigned long len = vma_desc_size(desc);
if (!vma_desc_test_flags(desc, VMA_SHARED_BIT, VMA_MAYSHARE_BIT))
return -EINVAL;
vma_desc_set_flags(desc, VMA_LOCKED_BIT, VMA_DONTDUMP_BIT);
if (!mlock_future_ok(desc->mm, /* VMA 고정 여부 = */ true, len))
return -EAGAIN;
desc->vm_ops = &secretmem_vm_ops;
return 0;
흐름:
1. VMA_SHARED_BIT | VMA_MAYSHARE_BIT 확인 → 공유 매핑만 허용
2. VMA_LOCKED_BIT | VMA_DONTDUMP_BIT 설정 → VMA 고정 + 코어 덤프 제외
3. mlock_future_ok() 확인 → mlock 한도 초과 시 -EAGAIN
4. desc->vm_ops = &secretmem_vm_ops 연결
제약 조건: secretmem은 반드시 shared 매핑으로만 사용할 수 있다.
// mm/secretmem.c:190-222
static struct file *secretmem_file_create(unsigned long flags)
역할: memfd_secret 시스템 호출의 핵심으로, anonymous inode 기반 파일을 생성한다.
// mm/secretmem.c:196-215
inode = anon_inode_make_secure_inode(secretmem_mnt->mnt_sb, anon_name, NULL);
if (IS_ERR(inode))
return ERR_CAST(inode);
file = alloc_file_pseudo(inode, secretmem_mnt, "secretmem",
O_RDWR | O_LARGEFILE, &secretmem_fops);
if (IS_ERR(file))
goto err_free_inode;
mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER);
mapping_set_unevictable(inode->i_mapping);
inode->i_op = &secretmem_iops;
inode->i_mapping->a_ops = &secretmem_aops;
/* zero size인 일반 파일처럼 보이게 처리 */
inode->i_mode |= S_IFREG;
inode->i_size = 0;
atomic_inc(&secretmem_users);
흐름:
1. anon_inode_make_secure_inode()로 보안 컨텍스트가 적용된 anonymous inode 생성
2. alloc_file_pseudo()로 pseudo 파일 할당
3. mapping_set_gfp_mask(GFP_HIGHUSER) → 고유 메모리에서만 할당
4. mapping_set_unevictable() → 회수 불가능하도록 설정
5. inode에 secretmem_iops, secretmem_aops 연결
6. atomic_inc(&secretmem_users)로 사용자 수 증가
// mm/secretmem.c:224-238
SYSCALL_DEFINE1(memfd_secret, unsigned int, flags)
역할: 사용자 공간의 memfd_secret(2) 시스템 호출을 처리한다.
// mm/secretmem.c:226-237
/* 로컬 flags가 전역 fcntl.h와 충돌하지 않는지 확인 */
BUILD_BUG_ON(SECRETMEM_FLAGS_MASK & O_CLOEXEC);
if (!secretmem_enable || !can_set_direct_map())
return -ENOSYS;
if (flags & ~(SECRETMEM_FLAGS_MASK | O_CLOEXEC))
return -EINVAL;
if (atomic_read(&secretmem_users) < 0)
return -ENFILE;
return FD_ADD(flags & O_CLOEXEC, secretmem_file_create(flags));
흐름:
1. secretmem_enable 및 can_set_direct_map() 확인 → 미지원 시 -ENOSYS
2. 플래그 유효성 검증 (mode/flag 마스크 + O_CLOEXEC)
3. secretmem_user 수 오버플로우 검사
4. secretmem_file_create() 호출 후 FD_ADD()로 fd 반환
반환: 성공 시 파일 디스크립터, 실패 시 errno.
// mm/secretmem.c:152-156
static void secretmem_free_folio(struct folio *folio)
역할: secretmem folio가 해제될 때 direct map을 복원하고 내용을 제로화한다.
흐름:
1. set_direct_map_default_noflush() → 커널 직접 매핑 복원
2. folio_zero_segment() → 메모리 내용 완전 제거 (데이터 누출 방지)
보안 중요성: 해제 전 제로화하지 않으면 다른 프로세스가 해당 물리 페이지를 할당받아 잔여 데이터를 읽을 수 있다.
사용자 공간: memfd_secret(O_CLOEXEC)
│
▼
SYSCALL_DEFINE1(memfd_secret)
├─ secretmem_enable / can_set_direct_map() 확인
├─ 플래그 검증
└─ secretmem_file_create()
├─ anon_inode_make_secure_inode() ← LSM 보안 컨텍스트 적용
├─ alloc_file_pseudo() ← pseudo 파일 할당
├─ mapping_set_gfp_mask(GFP_HIGHUSER) ← 커널 할당 제한
├─ mapping_set_unevictable() ← 회수 불가 설정
└─ atomic_inc(secretmem_users)
사용자 공간: mmap(fd, ...)
│
▼
secretmem_mmap_prepare()
├─ VMA_SHARED 검증
├─ VM_LOCKED | VM_DONTDUMP 설정
├─ mlock_future_ok() 확인
└─ vm_ops = &secretmem_vm_ops
사용자 공간: 접근 (페이지 폴트)
│
▼
secretmem_fault()
├─ filemap_lock_folio() 탐색
├─ [없으면] folio_alloc(__GFP_ZERO)
├─ set_direct_map_invalid_noflush() ← 커널 매핑 제거
├─ filemap_add_folio() ← 매핑에 추가
└─ flush_tlb_kernel_range() ← TLB 무효화
GUP 경로 (커널 직접 접근 차단):
├─ vma_is_secretmem(vma) → -EFAULT ← gup.c:1219
└─ secretmem_mapping(mapping) → false ← gup_fast_folio_allowed():2803
해제 시:
close(fd)
└─ secretmem_release()
└─ atomic_dec(secretmem_users)
folio 해제 시:
secretmem_free_folio()
├─ set_direct_map_default_noflush() ← direct map 복원
└─ folio_zero_segment() ← 데이터 제로화
| 플래그 | 허용 여부 | 설명 |
|---|---|---|
| `O_CLOEXEC` | 허용 | exec() 시 fd 자동 닫기 |
| `O_RDWR` | 허용 (기본값) | 읽기/쓰기 모드 |
| 기타 flags | 거부 (`-EINVAL`) | mode 플래그는 현재 미사용 (0x0) |
| 아키텍처 | `can_set_direct_map()` | `set_direct_map_invalid_noflush()` | 비고 |
|---|---|---|---|
| x86_64 | `true` (기본값) | `__set_pages_np()` | 완전 지원 |
| ARM64 | 조건부 | 조건부 | `rodata_full` 또는 KFENCE 또는 Realm |
| RISC-V | `true` (기본값) | 지원 | 기본 지원 |
| s390 | `true` (기본값) | 지원 | 기본 지원 |
| LoongArch | `true` (기본값) | 지원 | 기본 지원 |
| 경로 | 동작 | 코드 위치 |
|---|---|---|
| GUP (slow path) | `vma_is_secretmem()` → `-EFAULT` 반환 | `gup.c:1219` |
| GUP-fast (order-0 folio) | `secretmem_mapping()` → `false` 반환 (거부) | `gup.c:2803` |
| GUP-fast (large folio) | 검사 건너뜀 (secretmem은 order-0만 사용) | `gup.c:2756` |
| GUP-fast (hugetlb) | 건너뜀 (secretmem 불가) | `gup.c:2766` |
| 특성 | Secret Memory | mlock | memfd_create | sealed memfd |
|---|---|---|---|---|
| 커널 직접 매핑 제거 | **예** | 아니오 | 아니오 | 아니오 |
| GUP 차단 | **예** | 아니오 | 아니오 | 아니오 |
| 회수 불가 | **예** | 아니오 | 아니오 | 아니오 |
| 코어 덤프 제외 | **예** | 아니오 | 아니오 | 아니오 |
| LSM 보안 컨텍스트 | **예** | 아니오 | 아니오 | 아니오 |
| 사용 사례 | 암호화 키 저장 | 스왑 방지 | 임의 파일 | 리소스 보호 |
mm/secretmem.c (전체 270줄 — 비교적 작고 집중적인 구현)Documentation/mm/secretmem.rstmemfd_secret(2) man pageanon_inode_make_secure_inode()를 통한 SELinux/AppArmor 컨텍스트 적용