커널에서 use-after-free나 buffer overflow 같은 힙 메모리 손상은 재현이 어렵고, 실제 원인이 된 코드와 증상이 나타나는 시점이 멀리 떨어져 있어 디버깅하기 까다롭다. SLUB 할당자는 이런 손상을 조기에 잡아내기 위한 디버그 기능을 자체적으로 내장하고 있다. 이 글에서는 SLUB Debug의 동작 원리와 켜는 방법, 리포트를 읽는 방법을 정리한다.
SLUB Debug란
SLUB Debug를 켜면 각 오브젝트 앞뒤에 redzone(가드 영역)을 붙이고, 할당/해제 시점마다 그 영역이 침범당하지 않았는지, free된 오브젝트가 poison 패턴으로 채워진 그대로인지를 검사한다. 검사에 실패하면 해당 시점에 바로 BUG 리포트를 남기므로, 실제 오버플로우가 발생한 코드 근처에서 바로 잡아낼 수 있다는 장점이 있다.
커널 빌드 옵션 (Kconfig)
CONFIG_SLUB_DEBUG=y
CONFIG_SLUB_DEBUG_ON=y
CONFIG_SLUB_DEBUG는 디버그 기능 자체를 커널에 포함할지를 정하며(대부분의 배포판 커널에서 기본으로 켜져 있다), 이것만으로는 디버깅이 활성화되지 않고 아래에서 다루는 slab_debug= 부팅 파라미터가 있어야 실제로 켜진다. CONFIG_SLUB_DEBUG_ON을 함께 켜면 부팅 파라미터 없이도 기본값(FZPU, 아래 표 참고)으로 항상 켜진 채로 부팅되고, 필요할 때만 slab_debug=- 부팅 파라미터로 끌 수 있다.
slab_debug= 부팅 파라미터
실제로 어떤 검사를 켤지는 부팅 커맨드라인의 slab_debug=(옛 이름 slub_debug=도 동일하게 동작)로 지정한다. 각 알파벳은 서로 다른 검사를 의미한다.
| Flag | Description |
| F | Sanity checks. 오브젝트 개수, freelist 등 캐시 메타데이터 정합성 검사 |
| Z | Red zoning. 오브젝트 앞뒤에 redzone을 두고 침범 여부 검사 |
| P | Poisoning. 할당/해제 시 오브젝트를 특정 패턴으로 채워 미초기화 접근이나 use-after-free를 검사 |
| U | User tracking. 오브젝트를 마지막으로 할당/해제한 콜 스택을 기록 |
| T | Trace. 해당 캐시의 모든 할당/해제를 커널 로그에 출력 |
| A | failslab 대상으로 등록(fault injection 프레임워크와 연동) |
| O | 디버깅으로 인해 캐시의 최소 slab order가 증가하는 경우, 해당 캐시는 디버깅에서 제외 |
| – | 모든 디버깅을 끔 |
아무 옵션 없이 slab_debug만 주면 F, Z, P, U가 기본으로 켜진다(T, A는 로그량이 많거나 별도 프레임워크가 필요해서 기본에서 빠져 있다).
slab_debug # 모든 캐시에 기본 디버깅(F,Z,P,U) 적용
slab_debug=FZ,kmalloc-* # kmalloc-* 캐시에만 F,Z만 적용
slab_debug=FZPU,dentry;T,ext4_inode_cache # 세미콜론으로 캐시별로 다른 옵션 지정
slab_debug=- # 모든 디버깅 끔(CONFIG_SLUB_DEBUG_ON일 때 끄는 용도)
콤마 뒤에 캐시 이름을 나열하면 그 캐시들에만 옵션이 적용되고, 세미콜론으로 블록을 나누면 캐시 그룹마다 다른 디버그 옵션을 줄 수 있다. 캐시 이름은 /proc/slabinfo나 /sys/kernel/slab/ 아래 디렉토리 이름으로 확인할 수 있다.
런타임에서 확인하기 (/sys/kernel/slab)
캐시별로 어떤 디버그 옵션이 켜져 있는지는 /sys/kernel/slab/<cache-name>/ 아래에서 확인할 수 있다.
# cat /sys/kernel/slab/kmalloc-64/sanity_checks
# cat /sys/kernel/slab/kmalloc-64/red_zone
# cat /sys/kernel/slab/kmalloc-64/poison
# cat /sys/kernel/slab/kmalloc-64/store_user
# cat /sys/kernel/slab/kmalloc-64/trace
이 파일들은 대부분 읽기 전용이다. 즉 디버그 플래그는 캐시가 만들어지는 시점(slab_debug= 파싱 시점)에 정해지고, 이미 만들어진 캐시에 나중에 디버깅을 새로 켤 수는 없다. 예외적으로 validate는 쓰기가 가능한데, 여기에 아무 값이나 써 넣으면 그 시점에 캐시 전체를 대상으로 정합성 검사를 한 번 수행한다.
# echo 1 > /sys/kernel/slab/kmalloc-64/validate
손상 리포트 읽는 법
검사에 실패하면 아래와 같은 형식으로 리포트가 출력된다.
=============================================================================
BUG kmalloc-64 (Tainted: G): [Right Redzone overwritten] 0xffff888... First byte 0x41 instead of 0xcc
-----------------------------------------------------------------------------
Disabling lock debugging due to kernel taint
Object 0xffff888... @offset=64 fp=0xffff888...
Redzone ...
Object ...
Redzone ...
Padding ...
첫 줄의 캐시 이름(kmalloc-64)으로 어느 크기대의 오브젝트에서 문제가 났는지 알 수 있고, 대괄호 안의 문구가 어떤 검사에서 걸렸는지를 말해준다. Left/Right Redzone overwritten이면 오브젝트 앞/뒤 경계를 벗어나 썼다는 뜻이고, Poison overwritten이면 이미 해제된 영역에 다시 접근했다는 뜻이다. store_user(U)를 함께 켜두면 이 리포트 위에 마지막으로 할당/해제한 콜 스택도 같이 출력되어, 실제 문제가 된 코드를 바로 특정할 수 있다.
주의사항 및 팁
- redzone과 poison은 오브젝트마다 여분의 메모리와 매 할당/해제마다 검사 오버헤드가 붙으므로, 운영 서버 전체에 기본값으로 켜두기보다는 문제를 재현할 특정 캐시(
slab_debug=FZPU,캐시이름)에만 좁혀서 켜는 것이 실용적이다. - 일부 캐시는 디버깅용 메타데이터가 붙으면 slab의 최소 order가 올라가 메모리 낭비가 커질 수 있다. 이런 캐시까지 전부 디버깅하고 싶지 않다면
O옵션으로 제외한다. - 디버그 플래그는 캐시 생성 시점에 고정되므로, 이미 부팅된 시스템에서
/sys/kernel/slab를 통해 새로 켤 수 없다. 다시 켜려면 재부팅하면서slab_debug=파라미터를 바꿔야 한다. T(trace)는 해당 캐시의 모든 할당/해제를 로그로 남기기 때문에, 자주 쓰이는 캐시(kmalloc-* 등)에 걸어두면 로그가 순식간에 넘친다. 문제를 좁힌 특정 캐시에만 짧게 사용한다.