페이지 중독 (Page Poisoning)
개요 (Overview)
페이지 중독(Page Poisoning)은 커널이 해제된 페이지를 특정 바이트 값(0xaa)으로 덮어쓰는 디버깅 메커니즘입니다. 이 기능이 활성화되면 해제된 페이지가 PAGE_POISON 값으로 채워지고, 이후 다시 할당될 때 오염 여부를 확인해 UAF(Use-After-Free)나 덮어쓰기 흔적을 빠르게 찾을 수 있습니다.
이 메커니즘은 주로 개발/테스트 환경에서 사용되며, page_poison 부팅 파라미터와 CONFIG_PAGE_POISONING 설정을 통해 켭니다. 실제로는 include/linux/mm.h의 page_poisoning_enabled() / page_poisoning_enabled_static()와 mm/mm_init.c의 mem_debugging_and_hardening_init()가 init_on_alloc, init_on_free, debug_pagealloc과의 우선순위를 함께 정합니다. 페이지 중독은 하드웨어 오류 탐지(HWPoison)와는 다른 소프트웨어 수준의 안전장치입니다.
소스 파일 경로:
├── include/linux/mm.h ← page_poisoning_enabled() / want_init_on_free()
├── mm/page_poison.c ← 페이지 중독 핵심 구현
├── include/linux/poison.h ← PAGE_POISON 값 정의
├── mm/mm_init.c ← 초기화 및 static key 설정
└── kernel/power/snapshot.c ← hibernate 복구 시 사용
빠른 점검 명령
# 페이지 중독 활성화 여부 확인 (부팅 파라미터)
cat /proc/cmdline | grep -o "page_poison[= ]" || echo "비활성화"
# page_poison / init_on_free / debug_pagealloc 동시 확인
cat /proc/cmdline | tr ' ' '\n' | grep -E '^(page_poison|init_on_alloc|init_on_free|debug_pagealloc)=' || true
# CONFIG_PAGE_POISONING 확인
zcat /proc/config.gz 2>/dev/null | grep PAGE_POISONING || \
grep PAGE_POISONING /boot/config-$(uname -r) 2>/dev/null || echo "확인 불가"
# 커널 로그에서 페이지 중독 관련 메시지 확인
dmesg | grep -i "page.*poison\|poison.*page"
# init_on_free/page_poison 충돌 확인
dmesg | grep -i "mem auto-init.*will take precedence"
# 페이지 중독 관련 심볼 확인
cat /proc/kallsyms | grep -E "page_poisoning|_page_poison"
# debug_pagealloc과의 관계 확인
cat /proc/cmdline | grep -o "page_alloc_debug[= ]" || echo "미설정"
# 메모리 오염 관련 시스템 상태
grep -E "poison|corruption" /proc/meminfo 2>/dev/null || echo "해당 없음"
# 페이지 할당/해제 추적 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable 2>/dev/null
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_free/enable 2>/dev/null
# KASAN과의 동시 사용 확인
zcat /proc/config.gz 2>/dev/null | grep -E "KASAN|PAGE_POISON"
# 커널 메모리 디버그 옵션 확인
cat /proc/cmdline | tr ' ' '\n' | grep -E "poison|debug"
# 소스의 핵심 경로 재확인
grep -n "mem_debugging_and_hardening_init\|page_poisoning_enabled_static\|clear_or_poison_free_page" \
/home/ryotta205/Chamber_linux/linux-7.0-source/mm/mm_init.c \
/home/ryotta205/Chamber_linux/linux-7.0-source/include/linux/mm.h \
/home/ryotta205/Chamber_linux/linux-7.0-source/kernel/power/snapshot.c
핵심 자료구조
1. `_page_poisoning_enabled_early` (mm/page_poison.c:11)
bool _page_poisoning_enabled_early;
EXPORT_SYMBOL(_page_poisoning_enabled_early);
// 부팅 초기 단계에서 페이지 중독 활성화 여부 저장
// early_param("page_poison")으로 설정됨
// static_key 활성화 전까지 사용되는 임시 플래그
2. `_page_poisoning_enabled` (mm/page_poison.c:13)
DEFINE_STATIC_KEY_FALSE(_page_poisoning_enabled);
EXPORT_SYMBOL(_page_poisoning_enabled);
// 페이지 중독 기능의 정적 키 (static key)
// false: 비활성화 (기본값)
// true: 활성화 (page_poison 파라미터 또는 debug_pagealloc 활성화 시)
// 정적 키는 분기 예측 오버헤드를 제거하는 최적화 기법
3. `PAGE_POISON` (include/linux/poison.h:30)
#define PAGE_POISON 0xaa
// 페이지 중독에 사용되는 바이트 값 (16진수 0xaa = 10101010)
// 패턴 특성: 비트가 0과 1이 번갈아 가며 쓰레기 패턴 감지에 적합
// 다른 poison 값들:
// POISON_FREE = 0x6b (SLAB UAF 감지)
// SLUB_RED_INACTIVE = 0xbb (SLAB 레드존)
// SLUB_RED_ACTIVE = 0xcc (SLAB 활성 레드존)
4. `ratelimit_state` (mm/page_poison.c:50)
static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 10);
// 오염 감지 로그의 속도 제한
// 5초 내 최대 10회까지 출력
// 과도한 로그 생성을 방지하여 시스템 성능 보호
5. `page_poisoning_enabled()` / `page_poisoning_enabled_static()` / `want_init_on_free()` (include/linux/mm.h:4254, 4262, 4294)
#ifdef CONFIG_PAGE_POISONING
extern void __kernel_poison_pages(struct page *page, int numpages);
extern void __kernel_unpoison_pages(struct page *page, int numpages);
extern bool _page_poisoning_enabled_early;
DECLARE_STATIC_KEY_FALSE(_page_poisoning_enabled);
static inline bool page_poisoning_enabled(void)
{
return _page_poisoning_enabled_early;
}
static inline bool page_poisoning_enabled_static(void)
{
return static_branch_unlikely(&_page_poisoning_enabled);
}
DECLARE_STATIC_KEY_MAYBE(CONFIG_INIT_ON_FREE_DEFAULT_ON, init_on_free);
static inline bool want_init_on_free(void)
{
return static_branch_maybe(CONFIG_INIT_ON_FREE_DEFAULT_ON,
&init_on_free);
}
#endif
6. `mem_debugging_and_hardening_init()` (mm/mm_init.c:2541)
static void __init mem_debugging_and_hardening_init(void)
{
bool page_poisoning_requested = false;
bool want_check_pages = check_pages_enabled_early;
#ifdef CONFIG_PAGE_POISONING
/*
* Page poisoning is debug page alloc for some arches. If
* either of those options are enabled, enable poisoning.
*/
if (page_poisoning_enabled() ||
(!IS_ENABLED(CONFIG_ARCH_SUPPORTS_DEBUG_PAGEALLOC) &&
debug_pagealloc_enabled())) {
static_branch_enable(&_page_poisoning_enabled);
page_poisoning_requested = true;
want_check_pages = true;
}
#endif
if ((_init_on_alloc_enabled_early || _init_on_free_enabled_early) &&
page_poisoning_requested) {
pr_info("mem auto-init: CONFIG_PAGE_POISONING is on, "
"will take precedence over init_on_alloc and init_on_free\n");
_init_on_alloc_enabled_early = false;
_init_on_free_enabled_early = false;
}
if (_init_on_alloc_enabled_early) {
want_check_pages = true;
static_branch_enable(&init_on_alloc);
} else {
static_branch_disable(&init_on_alloc);
}
if (_init_on_free_enabled_early) {
want_check_pages = true;
static_branch_enable(&init_on_free);
} else {
static_branch_disable(&init_on_free);
}
if (IS_ENABLED(CONFIG_KMSAN) &&
(_init_on_alloc_enabled_early || _init_on_free_enabled_early))
pr_info("mem auto-init: please make sure init_on_alloc and init_on_free are disabled when running KMSAN\n");
if (debug_pagealloc_enabled()) {
want_check_pages = true;
static_branch_enable(&_debug_pagealloc_enabled);
if (debug_guardpage_minorder())
static_branch_enable(&_debug_guardpage_enabled);
}
}
역할: 초기 부트 옵션을 한 번에 모아 page_poison, init_on_alloc, init_on_free, debug_pagealloc의 정적 키를 정리합니다. page_poison이 켜지면 init_on_alloc/free보다 우선합니다.
핵심 함수
1. `poison_page()` — 페이지 오염 (mm/page_poison.c:22)
static void poison_page(struct page *page)
{
void *addr = kmap_local_page(page); // 페이지를 가상 주소로 매핑
kasan_disable_current(); // KASAN 현재 작업 비활성화
memset(kasan_reset_tag(addr), PAGE_POISON, PAGE_SIZE); // 0xaa로 채우기
kasan_enable_current(); // KASAN 재활성화
kunmap_local(addr); // 매핑 해제
}
역할: 단일 페이지의 전체 내용을 PAGE_POISON(0xaa) 값으로 덮어씁니다. KASAN이 페이지를 "사용 중"으로 인식하는 것을 방지하기 위해 임시로 비활성화합니다.
2. `__kernel_poison_pages()` — 다중 페이지 오염 (mm/page_poison.c:33)
void __kernel_poison_pages(struct page *page, int n)
{
int i;
for (i = 0; i < n; i++)
poison_page(page + i); // n개 페이지를 순차적으로 오염
}
역할: 연속된 n개 페이지를 오염시킵니다. 페이지 할당기에서 해제된 페이지를 처리할 때 호출됩니다.
3. `check_poison_mem()` — 오염 검증 (mm/page_poison.c:48)
static void check_poison_mem(struct page *page, unsigned char *mem, size_t bytes)
{
static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 10);
unsigned char *start;
unsigned char *end;
start = memchr_inv(mem, PAGE_POISON, bytes); // 오염 값이 아닌 첫 바이트 찾기
if (!start)
return; // 오염 패턴 정상
// 오염이 훼손된 범위 탐색
for (end = mem + bytes - 1; end > start; end--) {
if (*end != PAGE_POISON)
break;
}
if (!__ratelimit(&ratelimit))
return; // 속도 제한
if (start == end && single_bit_flip(*start, PAGE_POISON))
pr_err("pagealloc: single bit error\n"); // 단일 비트 오류
else
pr_err("pagealloc: memory corruption\n"); // 다중 바이트 손상
print_hex_dump(KERN_ERR, "", DUMP_PREFIX_ADDRESS, 16, 1, start,
end - start + 1, 1); // 손상된 영역 hex 덤프
dump_stack();
dump_page(page, "pagealloc: corrupted page details");
}
역할: 페이지가 해제된 후 누군가 손상시켰는지 검증합니다. memchr_inv로 오염 값이 아닌 첫 위치를 찾고, 단일 비트 오류(single_bit_flip)와 다중 바이트 손상을 구분하여 로그를 출력합니다.
4. `unpoison_page()` — 오염 해제 (mm/page_poison.c:76)
static void unpoison_page(struct page *page)
{
void *addr;
addr = kmap_local_page(page);
kasan_disable_current();
check_poison_mem(page, kasan_reset_tag(addr), PAGE_SIZE); // 오염 검증
kasan_enable_current();
kunmap_local(addr);
}
역할: 페이지를 BuddyAllocator에 반환하기 전에 오염 패턴이 유지되는지 검증합니다. check_poison_mem을 호출하여 손상 여부를 확인하고, 로그를 출력합니다.
5. `single_bit_flip()` — 단일 비트 오류 검출 (mm/page_poison.c:41)
static bool single_bit_flip(unsigned char a, unsigned char b)
{
unsigned char error = a ^ b;
return error && !(error & (error - 1)); // error가 2의 거듭제곱인지 검사
}
역할: 두 바이트 간 차이가 단일 비트인지 판별합니다. error & (error - 1)이 0이면 error가 2의 거듭제곱(단일 비트)임을 의미합니다.
호출 흐름
페이지 해제 시 오염 흐름
__free_pages()
└── __kernel_poison_pages(page, 1) ← 페이지 할당 해제 시
└── poison_page()
├── kmap_local_page() ← 가상 주소 매핑
├── kasan_disable_current() ← KASAN 비활성화
├── memset(PAGE_POISON) ← 0xaa로 채우기
├── kasan_enable_current() ← KASAN 재활성화
└── kunmap_local() ← 매핑 해제
페이지 재할당 시 검증 흐름
__alloc_pages()
└── __kernel_unpoison_pages(page, 1) ← 페이지 할당 시
└── unpoison_page()
├── kmap_local_page() ← 가상 주소 매핑
├── kasan_disable_current() ← KASAN 비활성화
├── check_poison_mem() ← 오염 패턴 검증
│ ├── memchr_inv() ← 오염 값이 아닌 위치 탐색
│ ├── single_bit_flip() ← 단일 비트 오류 검사
│ ├── pr_err() ← 손상 로그 출력
│ └── print_hex_dump() ← hex 덤프 출력
├── kasan_enable_current() ← KASAN 재활성화
└── kunmap_local() ← 매핑 해제
초기화 흐름 (mm/mm_init.c)
mem_debugging_and_hardening_init()
├── if (page_poisoning_enabled()) ← 부팅 파라미터 확인
│ └── static_branch_enable() ← 정적 키 활성화
├── if (init_on_alloc/free && page_poisoning)
│ └── init_on_alloc/free 비활성화 ← 중복 방지
└── check_pages = true ← 페이지 검증 활성화
hibernate 시 사용 (kernel/power/snapshot.c)
clear_or_poison_free_pages()
└── clear_or_poison_free_page()
├── if (page_poisoning_enabled_static())
│ └── __kernel_poison_pages(page, 1) ← 오염
└── else if (want_init_on_free())
└── clear_highpage(page) ← 초기화
페이지 복구 경로 (kernel/power/snapshot.c)
static void clear_or_poison_free_page(struct page *page)
{
if (page_poisoning_enabled_static())
__kernel_poison_pages(page, 1);
else if (want_init_on_free())
clear_highpage(page);
}
void clear_or_poison_free_pages(void)
{
...
if (page_poisoning_enabled() || want_init_on_free()) {
...
if (pfn_valid(pfn))
clear_or_poison_free_page(pfn_to_page(pfn));
}
}
역할: hibernate 복구 후 남은 free page를 오염시키거나 0으로 지워서, 복원 직후의 페이지 내용이 다음 사용에 섞이지 않게 합니다.
조건별 비교
페이지 중독 vs 관련 기능 비교
| 기능 | 페이지 중독 (Page Poisoning) | init_on_free | KASAN |
|---|---|---|---|
| **목적** | UAF 감지 (단순 패턴) | UAF 감지 (0으로 초기화) | 정밀 메모리 오류 탐지 |
| **오염 값** | 0xaa (단일 패턴) | 0x00 (영) | 복잡한 검증 패턴 |
| **성능 영향** | 낮음 (memset 1회) | 중간 (memset 1회) | 높음 (추적 구조체) |
| **CONFIG 옵션** | CONFIG_PAGE_POISONING | CONFIG_INIT_ON_ALLOC_ON_FREE | CONFIG_KASAN |
| **감지 정밀도** | 낮음 (패턴만 확인) | 중간 (초기화 확인) | 높음 (경로 추적) |
| **사용 환경** | 개발/테스트 | 개발/테스트 | 개발/디버깅 |
페이지 중독 활성화 경로 비교
| 경로 | 조건 | 동작 |
|---|---|---|
| **부팅 파라미터** | `page_poison=1` | `_page_poisoning_enabled_early = true` |
| **debug_pagealloc** | `CONFIG_ARCH_SUPPORTS_DEBUG_PAGEALLOC` 미지원 | `_page_poisoning_enabled` 활성화 |
| **수동 활성화** | `static_branch_enable()` | 정적 키 패치로 즉시 활성화 |
오염 검증 결과 비교
| 결과 | 의미 | 조치 |
|---|---|---|
| `memchr_inv`가 NULL 반환 | 오염 패턴 정상 | 별도 조치 없음 |
| 단일 비트 오류 | 하드웨어 비트 플립 의심 | 로그 출력 후 계속 |
| 다중 바이트 손상 | 메모리 손상 의심 | hex 덤프 + 스택 덤프 |
관련 문서
다이어그램
참고: poison 값 목록 (include/linux/poison.h)
| 값 | 이름 | 용도 |
|---|---|---|
| 0xaa | PAGE_POISON | 페이지 중독 (이 문서) |
| 0x6b | POISON_FREE | SLAB UAF 감지 |
| 0x5a | POISON_INUSE | SLAB 사용 중 감지 |
| 0xbb | SLUB_RED_INACTIVE | SLUB 비활성 레드존 |
| 0xcc | SLUB_RED_ACTIVE | SLUB 활성 레드존 |
| 0xa5 | POISON_END | SLAB 중독 끝 마커 |
| 0xa7 | POISON_POOL_FREED | DMA Pool 해제 |
| 0xa9 | POISON_POOL_ALLOCATED | DMA Pool 할당 |