페이지 중독 (Page Poisoning)

개요 (Overview)

페이지 중독(Page Poisoning)은 커널이 해제된 페이지를 특정 바이트 값(0xaa)으로 덮어쓰는 디버깅 메커니즘입니다. 이 기능이 활성화되면 해제된 페이지가 PAGE_POISON 값으로 채워지고, 이후 다시 할당될 때 오염 여부를 확인해 UAF(Use-After-Free)나 덮어쓰기 흔적을 빠르게 찾을 수 있습니다.

이 메커니즘은 주로 개발/테스트 환경에서 사용되며, page_poison 부팅 파라미터와 CONFIG_PAGE_POISONING 설정을 통해 켭니다. 실제로는 include/linux/mm.hpage_poisoning_enabled() / page_poisoning_enabled_static()mm/mm_init.cmem_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_freeKASAN
**목적**UAF 감지 (단순 패턴)UAF 감지 (0으로 초기화)정밀 메모리 오류 탐지
**오염 값**0xaa (단일 패턴)0x00 (영)복잡한 검증 패턴
**성능 영향**낮음 (memset 1회)중간 (memset 1회)높음 (추적 구조체)
**CONFIG 옵션**CONFIG_PAGE_POISONINGCONFIG_INIT_ON_ALLOC_ON_FREECONFIG_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 덤프 + 스택 덤프

관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 01-page_alloc.html — Buddy Allocator (페이지 할당/해제)
  • 27-debug.html — KASAN/KFENCE/KMEMLEAK 디버그 도구
  • 54-memory_leak.html — 메모리 누수 추적
  • 60-hwpoison.html — 하드웨어 오류 주입
  • 52-linux_6x_to_7x.html — Linux 6.x → 7.0 변경점

  • 다이어그램

    Page Poisoning 흐름
    자료구조 관계도

    참고: poison 값 목록 (include/linux/poison.h)

    이름용도
    0xaaPAGE_POISON페이지 중독 (이 문서)
    0x6bPOISON_FREESLAB UAF 감지
    0x5aPOISON_INUSESLAB 사용 중 감지
    0xbbSLUB_RED_INACTIVESLUB 비활성 레드존
    0xccSLUB_RED_ACTIVESLUB 활성 레드존
    0xa5POISON_ENDSLAB 중독 끝 마커
    0xa7POISON_POOL_FREEDDMA Pool 해제
    0xa9POISON_POOL_ALLOCATEDDMA Pool 할당