KASAN 사용법

KASAN(Kernel Address SANitizer)은 리눅스 커널에서 메모리 버그를 탐지하고 디버깅하는 도구로, 메모리 오버플로우 및 언더플로우, use-after-free 같은 일반적인 메모리 관련 오류를 검출하는 데 도움을 준다. 이 글에서는 KASAN 사용 방법에 대해 알아본다.

KASAN이란?

KASAN은 2015년 리눅스 커널 v4.0에 처음 병합된 컴파일러 계측 기반 메모리 오류 탐지 도구다. 커널 문서 기준으로 가장 널리 쓰이는 Generic KASAN은 GCC 8.3.0 이상(또는 커널이 지원하는 임의의 Clang 버전)을 요구하며, CONFIG_KASAN 옵션을 켜고 커널을 다시 빌드하면 커널 스스로 메모리 접근을 검사해 오버플로우·use-after-free 같은 오류를 즉시 리포트로 남긴다.

KASAN 사용법

커널 컴파일 옵션 설정

KASAN을 사용하려면 CONFIG_KASAN=y 옵션과 함께 커널 컴파일을 다시 해야 한다.

./scripts/config --enable KASAN
make

이때 CONFIG_WERROR=y(경고를 에러로 취급)가 함께 켜져 있으면 x86_64 KVM 코드에서 다음과 같은 빌드 에러가 날 수 있다. KASAN 계측이 들어가면서 kvm_arch_vm_ioctl 같은 함수의 스택 프레임 크기가 커지기 때문이다.

  CC [M]  arch/x86/kvm/../../../virt/kvm/binary_stats.o
  CC [M]  arch/x86/kvm/../../../virt/kvm/async_pf.o
  CC [M]  arch/x86/kvm/x86.o
arch/x86/kvm/x86.c: In function 'kvm_arch_vm_ioctl':
arch/x86/kvm/x86.c:6293:1: error: the frame size of 1176 bytes is larger than 1024 bytes [-Werror=frame-larger-than=]
 6293 | }
      | ^
cc1: all warnings being treated as errors
make[2]: *** [scripts/Makefile.build:297: arch/x86/kvm/x86.o] Error 1
make[1]: *** [scripts/Makefile.build:560: arch/x86/kvm] Error 2
make: *** [Makefile:1909: arch/x86] Error 2

KVM 쪽 Kconfig는 KVM_WERROR가 KASAN과 함께일 때는 자동으로 꺼지도록 돼 있지만, 커널 전체에 WERROR=y를 강제해두면 이 옵션도 같이 켜져 버린다. 이럴 때는 KVM만 개별적으로 -Werror를 끄면 된다.

./scripts/config --disable KVM_WERROR
make

KASAN 모드 종류

KASAN은 검사 방식에 따라 세 가지 모드로 나뉘고, 커널 설정에서 이 중 하나만 고를 수 있다.

  • Generic KASAN(CONFIG_KASAN_GENERIC): x86_64, arm, arm64, powerpc, riscv, s390, xtensa, loongarch에서 쓸 수 있는 소프트웨어 방식으로, GCC 8.3.0 이상이 필요하다. 오버헤드가 크지만 slab·page_alloc·vmalloc·스택·전역 변수까지 모든 메모리 종류를 검사하는 가장 완전한 모드라 개발 중 디버깅 용도로 가장 많이 쓰인다.
  • Software Tag-Based KASAN(CONFIG_KASAN_SW_TAGS): arm64 전용이며 GCC 11 이상이 필요하다. 유저스페이스의 HWASan과 비슷한 방식으로 메모리에 태그를 붙여 검사하며, Generic보다 오버헤드가 낮아 메모리가 제한적인 디바이스 테스트에도 쓸 수 있다.
  • Hardware Tag-Based KASAN(CONFIG_KASAN_HW_TAGS): ARM MTE(Memory Tagging Extension) 하드웨어가 있는 arm64에서만 동작하며 GCC 10 이상 또는 Clang 12 이상이 필요하다. 오버헤드가 낮아 세 모드 중 유일하게 프로덕션 환경에서 상시 켜두는 것도 고려할 수 있는 모드다.

소프트웨어 모드(Generic/SW_TAGS)에서는 계측 방식도 CONFIG_KASAN_OUTLINE(바이너리 크기가 작음)과 CONFIG_KASAN_INLINE(최대 2배 빠름) 중에서 고를 수 있다.

런타임 옵션 (커널 커맨드라인)

Generic·SW_TAGS 모드는 컴파일 타임에 동작이 고정되지만, Hardware Tag-Based KASAN은 부팅 이후에도 커맨드라인 파라미터로 세부 동작을 조절할 수 있다.

  • kasan=off|on: HW_TAGS 모드에서만 유효한 옵션으로, KASAN 검사 자체를 껐다 켰다 할 수 있다(기본값 on).
  • kasan.mode=sync|async|asymm: 태그 불일치를 동기적으로 즉시 검출할지(sync), 비동기로 나중에 검출할지(async), 읽기는 동기·쓰기는 비동기로 처리할지(asymm) 정한다.
  • kasan.vmalloc=off|on: vmalloc 영역까지 태깅해서 검사할지 여부.
  • kasan.fault=report|panic|panic_on_write: 버그를 발견했을 때 리포트만 남길지(기본값), 커널을 패닉시킬지, 쓰기 오류일 때만 패닉시킬지 정한다.
  • kasan_multi_shot: 기본값은 첫 버그를 발견하면 리포트 후 검사를 멈추는데, 이 옵션을 주면 이후에도 계속 검사해 여러 버그를 한 번에 확인할 수 있다.

KASAN 사용 예제

아래 예제들은 커널 모듈로 빌드해 insmod로 올리면 바로 KASAN 리포트를 확인할 수 있다. Makefile은 세 예제 모두 동일하게 쓸 수 있다.

obj-m += kasan_demo.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

전역 버퍼 오버플로우 (Global Buffer Overflow)

buffer가 함수 밖에 선언된 전역(static) 배열이라, 여기에 범위를 넘겨 쓰면 KASAN은 global-out-of-bounds 오류로 리포트한다.

#include <linux/module.h>
#include <linux/string.h>

static char buffer[10];

int init_module(void)
{
	strcpy(buffer, "Overflow!"); // 10바이트 배열에 널 종료 포함 10바이트 기록 → 범위 초과
	printk(KERN_INFO "Buffer: %s\n", buffer);
	return 0;
}

void cleanup_module(void)
{
	printk(KERN_INFO "Module unloaded\n");
}

MODULE_LICENSE("GPL");

스택 배열 범위 초과 읽기 (Stack Out-of-Bounds Read)

arr는 함수 안에 선언된 지역(스택) 배열이라, 이 경우 KASAN은 stack-out-of-bounds로 리포트한다.

#include <linux/module.h>

int init_module(void)
{
	int arr[5] = {1, 2, 3, 4, 5};
	int value = arr[6]; // 배열 범위(0~4)를 벗어난 읽기

	printk(KERN_INFO "Value: %d\n", value);
	return 0;
}

void cleanup_module(void)
{
	printk(KERN_INFO "Module unloaded\n");
}

MODULE_LICENSE("GPL");

Use-After-Free

앞의 두 예제는 정적/스택 메모리 범위를 벗어난 경우고, KASAN이 가장 널리 쓰이는 이유는 사실 이 use-after-free 탐지 때문이다. kfree()로 해제한 메모리에 다시 접근하면 slab-use-after-free로 리포트된다.

#include <linux/module.h>
#include <linux/slab.h>

int init_module(void)
{
	int *ptr = kmalloc(sizeof(int), GFP_KERNEL);

	if (!ptr)
		return -ENOMEM;

	kfree(ptr);
	*ptr = 1; // 이미 해제된 메모리에 쓰기

	printk(KERN_INFO "Value: %d\n", *ptr);
	return 0;
}

void cleanup_module(void)
{
	printk(KERN_INFO "Module unloaded\n");
}

MODULE_LICENSE("GPL");

KASAN 로그 확인

모듈을 insmod kasan_demo.ko로 올리면 dmesg에 다음과 같은 형태의 리포트가 남는다(use-after-free 예제 기준으로 축약).

BUG: KASAN: slab-use-after-free in init_module+0x2a/0x50 [kasan_demo]
Write of size 4 at addr ffff8881xxxxxxxx by task insmod/1234

CPU: 0 PID: 1234 Comm: insmod
Call Trace:
 kasan_report+0x...
 init_module+0x2a/0x50 [kasan_demo]
 do_one_initcall+0x...

Allocated by task 1234:
 kmalloc+0x...
 init_module+0x1a/0x50 [kasan_demo]

Freed by task 1234:
 kfree+0x...
 init_module+0x20/0x50 [kasan_demo]

The buggy address belongs to the object at ffff8881xxxxxxxx
 which belongs to the cache kmalloc-8 of size 8

Memory state around the buggy address:
 ffff8881xxxxxxx0: fa fb fb fb fb fb fb fb
>ffff8881xxxxxxx8: fb fb fb fb fb fb fb fb
                   ^
 ffff8881xxxxxxxx: fb fb fb fb fb fb fb fb

리포트는 위에서부터 버그 종류와 위치(slab-use-after-free in init_module), 문제가 발생한 시점의 콜 스택, 할당(Allocated by)·해제(Freed by) 시점의 콜 스택, 그리고 버그 주소가 속한 슬랩 객체 정보 순으로 읽으면 된다. 맨 아래 fb 같은 값은 shadow memory 바이트로, 해당 8바이트 구간이 이미 해제되어(free) 접근할 수 없는 상태라는 것을 뜻하며 ^ 표시가 실제로 접근을 시도한 바이트 위치다.

주의사항

  • Generic/Software Tag-Based KASAN은 메모리·성능 오버헤드가 크기 때문에 개발·테스트 커널에서만 켜야 하고, 실제 서비스용 커널에 그대로 넣으면 안 된다. 프로덕션에서 상시 검사가 목적이라면 오버헤드가 낮은 Hardware Tag-Based KASAN(ARM MTE 필요)을 고려해야 한다.
  • 커널 모듈로 테스트할 때는 모듈을 빌드한 커널과 실제 부팅한 커널이 CONFIG_KASAN 설정까지 정확히 일치해야 한다. 배포판 기본 커널에는 대부분 KASAN이 꺼져 있으므로, 직접 CONFIG_KASAN=y로 빌드한 커널로 재부팅한 뒤 테스트해야 한다.
  • 기본 동작은 첫 버그를 리포트하면 검사를 멈추는 것이므로, 여러 버그를 한 번에 확인하려면 kasan_multi_shot 부트 파라미터를 추가해야 한다.
  • kasan=off|onkasan.mode= 같은 런타임 파라미터는 Hardware Tag-Based KASAN 전용이다. Generic이나 Software Tag-Based 모드는 컴파일 타임에 고정되므로 부팅 파라미터로 켜고 끌 수 없다.

마무리

KASAN은 커널 컴파일 옵션 하나로 오버플로우, use-after-free 같은 메모리 버그를 재현 즉시 정확한 위치와 콜 스택까지 짚어주는 도구다. 오버헤드 때문에 프로덕션에 그대로 켜두기는 어렵지만, 커널 모듈이나 드라이버를 개발하면서 디버깅 커널 하나를 따로 두고 KASAN을 켜두면 syzkaller 같은 퍼저와 함께 써서 메모리 버그를 훨씬 빨리 잡아낼 수 있다.

참고

답글 남기기