페이지 테이블 검증 (Page Table Check)
개요 (Overview)
Linux 커널의 페이지 테이블 검증(Page Table Check)은 사용자 페이지 테이블에 대한 잘못된 매핑을 실시간으로 감지하는 디버깅 메커니즘입니다. 이 기능은 anonymous 페이지의 이중 쓰기 매핑(Double Write Mapping)과 anonymous/file 페이지의 부당한 공유를 탐지하여, 메모리 누수, 데이터 손상, 무결성 문제를 동기적으로 발견합니다.
페이지 테이블 검증은 page_table_check.c에서 구현되며, 페이지 테이블 엔트리(PTE/PMD/PUD)가 설정되거나 해제될 때마다 호출되어 카운터 기반의 일관성 검사를 수행합니다. 이 메커니즘은 CONFIG_PAGE_TABLE_CHECK 커널 컨피그 옵션으로 제어되며, x86_64, ARM64, RISC-V, PowerPC 아키텍처를 지원합니다. page_table_check=on 부트 파라미터 또는 CONFIG_PAGE_TABLE_CHECK_ENFORCED 옵션으로 활성화할 수 있습니다.
소스 파일 경로:
├── mm/page_table_check.c ← 검증 코어 로직
├── include/linux/page_table_check.h ← 외부 인터페이스
├── include/linux/page_ext.h ← 페이지 확장 프레임워크
├── include/linux/leafops.h ← 소프트 리프 엔트리 연산
└── mm/Kconfig.debug ← 커널 컨피그 옵션
빠른 점검 명령
# 페이지 테이블 검증 활성화 상태 확인
cat /proc/cmdline | grep page_table_check
# 커널 컨피그에서 PAGE_TABLE_CHECK 확인
grep PAGE_TABLE_CHECK /boot/config-$(uname -r)
# 페이지 테이블 검증 관련 커널 로그 확인
dmesg | grep -i "page_table_check"
# page_ext 메모리 사용량 확인
cat /proc/meminfo | grep -i "page_tables"
# 페이지 테이블 관련 통계 확인
cat /proc/vmstat | grep -i "pgfault\|pgmajfault"
# 페이지 테이블 검증 활성화 (부트 파라미터)
# GRUB 설정에 추가: page_table_check=on
# 현재 페이지 테이블 검증 상태 확인 (커널 빌드 시)
grep CONFIG_PAGE_TABLE_CHECK /boot/config-$(uname -r)
# 페이지 확장 메모리 사용량 확인
cat /sys/kernel/debug/page_ext/
# 페이지 테이블 엔트리 수 확인
cat /proc/vmstat | grep -i "nr_pte\|nr_pmd\|nr_pud"
핵심 자료구조
1. `struct page_table_check` (mm/page_table_check.c:16)
struct page_table_check {
atomic_t anon_map_count; // anonymous 페이지 매핑 카운터
atomic_t file_map_count; // 파일 기반 페이지 매핑 카운터
};
// 각 페이지마다 page_ext에 연결되어 관리됨
// anon과 file은 서로 배타적이어야 함 (동시 증가 시 BUG_ON)
2. `struct page_ext_operations` (include/linux/page_ext.h:25)
struct page_ext_operations {
size_t offset; // page_ext 내 클라이언트 데이터 오프셋
size_t size; // 클라이언트 데이터 크기
bool (*need)(void); // page_ext 필요 여부 함수 포인터
void (*init)(void); // 초기화 함수 포인터 (선택)
bool need_shared_flags; // 공유 플래그 필드 사용 여부
};
// page_table_check_ops는 이 구조체를 통해 page_ext 프레임워크에 등록됨
3. `struct page_ext` (include/linux/page_ext.h:52)
struct page_ext {
unsigned long flags; // 페이지 확장 플래그
};
// 모든 페이지에 대해 page_ext가 할당되며, pfn에 대응됨
// page_table_check는 이 구조체의 확장 데이터로 저장됨
4. `struct page_ext_iter` (include/linux/page_ext.h:113)
struct page_ext_iter {
unsigned long index; // 현재 순회 인덱스
unsigned long start_pfn; // 시작 페이지 프레임 번호
struct page_ext *page_ext; // 현재 page_ext 포인터
};
// for_each_page_ext 매크로에서 사용되는 이터레이터
5. `page_table_check_disabled` (mm/page_table_check.c:24)
DEFINE_STATIC_KEY_TRUE(page_table_check_disabled);
// 정적 키 기반 분기 최적화
// 기본값: true (검증 비활성화)
// init_page_table_check()에서 false로 변경 시 검증 활성화
6. `__page_table_check_enabled` (mm/page_table_check.c:21)
static bool __page_table_check_enabled __initdata =
IS_ENABLED(CONFIG_PAGE_TABLE_CHECK_ENFORCED);
// 커널 부팅 시 초기화되는 활성화 플래그
// CONFIG_PAGE_TABLE_CHECK_ENFORCED: 항상 활성화
// page_table_check=on 부트 파라미터: 선택적 활성화
핵심 함수
1. `page_table_check_clear()` (mm/page_table_check.c:63)
static void page_table_check_clear(unsigned long pfn, unsigned long pgcnt)
{
struct page_ext_iter iter;
struct page_ext *page_ext;
struct page *page;
bool anon;
if (!pfn_valid(pfn))
return;
page = pfn_to_page(pfn);
BUG_ON(PageSlab(page)); // Slab 페이지는 검증 대상 아님
anon = PageAnon(page); // anonymous/file 구분
rcu_read_lock();
for_each_page_ext(page, pgcnt, page_ext, iter) {
struct page_table_check *ptc = get_page_table_check(page_ext);
if (anon) {
BUG_ON(atomic_read(&ptc->file_map_count)); // anonymous인데 file 카운터 존재 시 BUG
BUG_ON(atomic_dec_return(&ptc->anon_map_count) < 0); // 음수 카운터 시 BUG
} else {
BUG_ON(atomic_read(&ptc->anon_map_count)); // file인데 anon 카운터 존재 시 BUG
BUG_ON(atomic_dec_return(&ptc->file_map_count) < 0); // 음수 카운터 시 BUG
}
}
rcu_read_unlock();
}
// 역할: PTE/PMD/PUD 제거 시 해당 페이지의 매핑 카운터를 감소시키고 검증
// 호출자: __page_table_check_pte_clear, __page_table_check_pmd_clear, __page_table_check_pud_clear
2. `page_table_check_set()` (mm/page_table_check.c:97)
static void page_table_check_set(unsigned long pfn, unsigned long pgcnt,
bool rw)
{
struct page_ext_iter iter;
struct page_ext *page_ext;
struct page *page;
bool anon;
if (!pfn_valid(pfn))
return;
page = pfn_to_page(pfn);
BUG_ON(PageSlab(page));
anon = PageAnon(page);
rcu_read_lock();
for_each_page_ext(page, pgcnt, page_ext, iter) {
struct page_table_check *ptc = get_page_table_check(page_ext);
if (anon) {
BUG_ON(atomic_read(&ptc->file_map_count)); // anonymous인데 file 카운터 존재 시 BUG
BUG_ON(atomic_inc_return(&ptc->anon_map_count) > 1 && rw); // anonymous 쓰기 이중 매핑 시 BUG
} else {
BUG_ON(atomic_read(&ptc->anon_map_count)); // file인데 anon 카운터 존재 시 BUG
BUG_ON(atomic_inc_return(&ptc->file_map_count) < 0); // 음수 카운터 시 BUG
}
}
rcu_read_unlock();
}
// 역할: PTE/PMD/PUD 설정 시 해당 페이지의 매핑 카운터를 증가시키고 검증
// anonymous 페이지의 쓰기 이중 매핑 감지 (anon_map_count > 1 && rw)
3. `__page_table_check_zero()` (mm/page_table_check.c:131)
void __page_table_check_zero(struct page *page, unsigned int order)
{
struct page_ext_iter iter;
struct page_ext *page_ext;
BUG_ON(PageSlab(page));
rcu_read_lock();
for_each_page_ext(page, 1 << order, page_ext, iter) {
struct page_table_check *ptc = get_page_table_check(page_ext);
BUG_ON(atomic_read(&ptc->anon_map_count)); // 매핑 카운터가 0이 아니면 BUG
BUG_ON(atomic_read(&ptc->file_map_count)); // 매핑 카운터가 0이 아니면 BUG
}
rcu_read_unlock();
}
// 역할: 페이지 할당 시 또는 해제 시 카운터가 0인지 확인
// 호출자: page_table_check_alloc, page_table_check_free
4. `__page_table_check_ptes_set()` (mm/page_table_check.c:202)
void __page_table_check_ptes_set(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte, unsigned int nr)
{
unsigned int i;
if (&init_mm == mm) // init_mm는 검증 대상 아님
return;
page_table_check_pte_flags(pte); // uffd_wp 플래그 검증
for (i = 0; i < nr; i++)
__page_table_check_pte_clear(mm, addr + PAGE_SIZE * i, ptep_get(ptep + i));
if (pte_user_accessible_page(pte, addr))
page_table_check_set(pte_pfn(pte), nr, pte_write(pte));
}
// 역할: PTE 배치 시 기존 PTE 해제 및 새 PTE 설정 검증
// 페이지 폴트 처리, mmap, fork 등에서 호출됨
5. `__page_table_check_pmds_set()` (mm/page_table_check.c:231)
void __page_table_check_pmds_set(struct mm_struct *mm, unsigned long addr,
pmd_t *pmdp, pmd_t pmd, unsigned int nr)
{
unsigned long stride = PMD_SIZE >> PAGE_SHIFT;
unsigned int i;
if (&init_mm == mm)
return;
page_table_check_pmd_flags(pmd); // uffd_wp 플래그 검증
for (i = 0; i < nr; i++)
__page_table_check_pmd_clear(mm, addr + PMD_SIZE * i, *(pmdp + i));
if (pmd_user_accessible_page(pmd, addr))
page_table_check_set(pmd_pfn(pmd), stride * nr, pmd_write(pmd));
}
// 역할: PMD 배치 시 기존 PMD 해제 및 새 PMD 설정 검증
// THP(Transparent Huge Page) 할당 시 호출됨
6. `__page_table_check_pte_clear_range()` (mm/page_table_check.c:265)
void __page_table_check_pte_clear_range(struct mm_struct *mm,
unsigned long addr,
pmd_t pmd)
{
if (&init_mm == mm)
return;
if (!pmd_bad(pmd) && !pmd_leaf(pmd)) {
pte_t *ptep = pte_offset_map(&pmd, addr);
unsigned long i;
if (WARN_ON(!ptep))
return;
for (i = 0; i < PTRS_PER_PTE; i++) {
__page_table_check_pte_clear(mm, addr, ptep_get(ptep));
addr += PAGE_SIZE;
ptep++;
}
pte_unmap(ptep - PTRS_PER_PTE);
}
}
// 역할: PMD에 속한 모든 PTE를 범위 단위로 해제 검증
// 호출자: khugepaged (THP 병합/해제 시)
호출 흐름
PTE 설정 시 검증 흐름
페이지 폴트 / mmap / fork
│
▼
__page_table_check_ptes_set()
│
├── page_table_check_pte_flags() ← uffd_wp 플래그 검증
│
├── __page_table_check_pte_clear() ← 기존 PTE 해제 검증
│ │
│ ▼
│ page_table_check_clear() ← anon/file 카운터 감소
│ │
│ ▼
│ atomic_dec_return() ← 음수 카운터 검증
│
└── page_table_check_set() ← 새 PTE 설정 검증
│
▼
atomic_inc_return() ← 이중 매핑 검증
페이지 할당/해제 시 검증 흐름
__alloc_pages() / __free_pages()
│
▼
page_table_check_alloc() / page_table_check_free()
│
▼
__page_table_check_zero()
│
▼
atomic_read(anon_map_count) == 0? ── 아니면 → BUG_ON
atomic_read(file_map_count) == 0? ── 아니면 → BUG_ON
THP 병합/해제 시 검증 흐름
khugepaged (THP 병합/해제)
│
▼
page_table_check_pte_clear_range()
│
▼
PMD 유효성 검사 (!pmd_bad && !pmd_leaf)
│
▼
PTRS_PER_PTE 순회
│
├── __page_table_check_pte_clear() ← 각 PTE 해제 검증
│ │
│ ▼
│ page_table_check_clear() ← 카운터 감소
│
└── 완료
조건별 비교
1. 검증 대상별 비교
| 구분 | PTE (4KB) | PMD (2MB) | PUD (1GB) |
| **검증 함수** | `__page_table_check_ptes_set` | `__page_table_check_pmds_set` | `__page_table_check_puds_set` |
| **플래그 검증** | `page_table_check_pte_flags` | `page_table_check_pmd_flags` | 없음 |
| **페이지 수 계산** | `nr` (직접) | `stride * nr` (PMD_SIZE >> PAGE_SHIFT) | `stride * nr` (PUD_SIZE >> PAGE_SHIFT) |
| **사용 시나리오** | 일반 페이지 폴트, mmap | THP 할당 | HugeTLB 할당 |
| **호출 빈도** | 가장 높음 | 중간 | 낮음 |
2. 활성화 방식 비교
| 방식 | 컨피그 옵션 | 부트 파라미터 | 동작 |
| **기본 비활성화** | `CONFIG_PAGE_TABLE_CHECK=y` | 없음 | 필요 시 `page_table_check=on`으로 활성화 |
| **강제 활성화** | `CONFIG_PAGE_TABLE_CHECK_ENFORCED=y` | 불필요 | 항상 활성화 |
| **동적 활성화** | `CONFIG_PAGE_TABLE_CHECK=y` | `page_table_check=on` | 부팅 시 동적 활성화 |
3. 아키텍처 지원 비교
| 아키텍처 | 지원 여부 | 컨피그 위치 | 비고 |
| **x86_64** | 지원 | `arch/x86/Kconfig` | 64비트만 지원 |
| **ARM64** | 지원 | `arch/arm64/Kconfig` | 전체 지원 |
| **RISC-V** | 지원 | `arch/riscv/Kconfig` | MMU 필요 |
| **PowerPC** | 조건부 지원 | `arch/powerpc/Kconfig` | HugeTLB 미사용 시 지원 |
| **32비트** | 미지원 | - | - |
4. 플래그 검증 조건 비교
| 플래그 | PTE 검증 | PMD 검증 | 동작 |
| **pte_uffd_wp** | `WARN_ON_ONCE(pte_uffd_wp && pte_write)` | - | uffd-wp와 쓰기 동시 설정 시 경고 |
| **pte_swp_uffd_wp** | `WARN_ON_ONCE(cached_writable)` | - | 스왑된 uffd-wp에 쓰기 가능 시 경고 |
| **pmd_uffd_wp** | - | `WARN_ON_ONCE(pmd_uffd_wp && pmd_write)` | PMD 수준 uffd-wp 검증 |
| **pmd_swp_uffd_wp** | - | `WARN_ON_ONCE(cached_writable)` | PMD 스왑 uffd-wp 검증 |
카운터 기반 검증 원리
anonymous/file 배타성 검증
모든 페이지는 다음 중 하나여야 함:
1. anonymous 매핑만 있음 (anon_map_count >= 0, file_map_count == 0)
2. file 매핑만 있음 (anon_map_count == 0, file_map_count >= 0)
위반 시 BUG_ON() 발생:
- anon인데 file_map_count > 0 → BUG
- file인데 anon_map_count > 0 → BUG
쓰기 이중 매핑 감지
anonymous 페이지의 경우:
- anon_map_count > 1 && rw == true → BUG_ON 발생
- 즉, 하나의 anonymous 페이지를 두 프로세스가 동시에 쓰기로 매핑하는 것 방지
file 페이지의 경우:
- file_map_count 증가만 검증 (쓰기 이중 매핑 검증 없음)
- file 페이지는 COW로 보호되므로 이중 쓰기 매핑이 발생하지 않음
빈 카운터 검증 (할당/해제 시)
페이지가 free list에 있거나 할당되는 시점:
- anon_map_count == 0 이어야 함
- file_map_count == 0 이어야 함
- 위반 시 BUG_ON() 발생 (메모리 누수 또는 초기화 실패 감지)
관련 문서
00 — 메모리 관리 개요
03 — VMA / mmap
10 — Huge Pages / THP
12 — Folio / Page Cache
27 — 디버그 도구
38 — Page Table Walk
SVG 다이어그램
자료구조 관계도
호출 흐름