UBSAN (Undefined Behavior Sanitizer)
UBSAN은 C 언어 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지하는 커널 Sanitizer입니다. 정수 오버플로, 배열 범위 초과, 시프트 오류, NULL 포인터 역참조 등 컴파일러 최적화(Compiler Optimization)에 의해 예측 불가능해지는 영역을 런타임 체크로 포착해, 보안 취약점(Vulnerability)으로 이어질 수 있는 케이스를 페이지(Page) 단위 로그로 남깁니다. 오버헤드(Overhead)가 낮아 프로덕션 디버깅(Debugging)에도 제한적으로 유지할 수 있습니다.
핵심 요약
- 런타임 탐지 — 컴파일러가 UB 발생 가능 지점에 체크 코드를 삽입, 실행 중 발견 시 리포트
- 낮은 오버헤드 — 옵션별로 다르지만 수 % 이내.
CONFIG_UBSAN_BOUNDS는 프로덕션 유지 가능 - 세분화된 옵션 — 오버플로/범위/정렬/시프트/NULL 등을 개별 CONFIG로 토글
- FORTIFY_SOURCE 보완 — 컴파일 타임 체크가 불가능한 동적 인덱스를 UBSAN이 런타임에 포착
- 의도적 UB 표기 — 래핑 산술이 필요한 곳은
wrapping_add/no_sanitize로 제외 표기
단계별 이해
- 컴파일러 계측
GCC/Clang이 각 UB 후보 연산 앞에__builtin_*_overflow체크 또는 범위 비교를 삽입합니다. - 실행
정상 실행 시 분기 예측(Branch Prediction) 덕분에 오버헤드가 무시할 만합니다. - UB 발견
체크가 실패하면__ubsan_handle_*런타임 핸들러(Handler)가 호출되며, NULL 역참조(Dereference) 같은 포인터 문제도 이 경로를 거칩니다. - 리포트
핸들러가 콜 스택·소스 위치·값을printk로 출력합니다.CONFIG_UBSAN_TRAP=y면 즉시 oops. - 대응
버그면 수정, 의도적 UB면wrapping_add/속성/UBSAN_SANITIZE := n으로 제외.
개요
UBSAN은 C 언어 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지합니다. UB는 컴파일러가 임의로 최적화할 수 있는 영역이므로 예측 불가능한 버그와 보안 취약점의 원인이 됩니다.
탐지 유형과 CONFIG 옵션
| 탐지 유형 | CONFIG 옵션 | 예시 | 위험도 |
|---|---|---|---|
| 정수 오버플로(Integer Overflow) — 부호 있음/없음 통합 | CONFIG_UBSAN_INTEGER_WRAP |
INT_MAX + 1, 0u - 1 |
높음 (보안 취약점) |
| 배열 인덱스 범위 초과 (상수 크기) | CONFIG_UBSAN_BOUNDS |
arr[size] (고정 크기 배열) |
높음 |
| 배열 범위 (GCC 엄격 모드) | CONFIG_UBSAN_BOUNDS_STRICT |
flex array·유사 배열까지 확장 | 높음 |
| 배열 범위 (Clang) | CONFIG_UBSAN_ARRAY_BOUNDS |
Clang의 array bounds 체크 | 높음 |
| 포인터 파생 로컬 배열 범위 (Clang) | CONFIG_UBSAN_LOCAL_BOUNDS |
컴파일 타임 추론 불가한 동적 인덱스 | 중간 |
| 정렬 오류 | CONFIG_UBSAN_ALIGNMENT |
4바이트 정렬 필요 타입의 비정렬 접근 | 중간 (아키텍처 의존) |
| 시프트 오류 | CONFIG_UBSAN_SHIFT |
1 << 33 (32비트 타입) |
중간 |
| 0 나누기 | CONFIG_UBSAN_DIV_ZERO |
a / 0, INT_MIN / -1 |
중간 |
| bool 잘못된 값 | CONFIG_UBSAN_BOOL |
0/1 외의 값이 bool로 로드 | 낮음 |
| enum 잘못된 값 | CONFIG_UBSAN_ENUM |
정의 범위 밖의 enum 값 | 낮음 |
| 도달 불가 코드 실행 | CONFIG_UBSAN_UNREACHABLE |
__builtin_unreachable() 실행 시 |
중간 |
CONFIG_UBSAN_OBJECT_SIZE 같은
별도 옵션이 있었으나, 현재 커널은 이들을 __ubsan_handle_type_mismatch_v1 핸들러에 통합해
CONFIG_UBSAN 활성화 시 자동으로 탐지합니다. 또한 정수 오버플로는 과거 SIGNED_WRAP/
UNSIGNED_WRAP으로 분리돼 있었으나 현재는 CONFIG_UBSAN_INTEGER_WRAP로 통합되었습니다.
컴파일러 계측 예시
/* UBSAN 정수 오버플로 탐지 예시 */
/* 취약 코드: 부호 있는 정수 오버플로 */
int vulnerable_calc(int a, int b)
{
return a + b; /* a=INT_MAX, b=1이면 UB! */
}
/* UBSAN이 삽입하는 체크 (개념적) */
int vulnerable_calc(int a, int b)
{
int result;
if (__builtin_add_overflow(a, b, &result))
__ubsan_handle_add_overflow(a, b); /* 리포트! */
return result;
}
/* UBSAN 배열 범위 탐지 예시 */
struct example {
int data[10];
};
void test_bounds(struct example *e, int idx)
{
/* idx=10이면 UBSAN이 탐지: array index 10 out of bounds */
e->data[idx] = 42;
}
/* UBSAN 커널 설정 */
/*
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y # 배열 범위 (가장 유용)
CONFIG_UBSAN_INTEGER_WRAP=y # 정수 오버플로 (부호 있음/없음 통합)
CONFIG_UBSAN_SHIFT=y # 시프트 오류
CONFIG_UBSAN_TRAP=n # 리포트만 (y이면 즉시 트랩)
*/
코드 설명
-
3-7행
컴파일러는
a + b처럼 UB 가능 연산의 원본 소스를 유지하되, 아래의 대체 형태로 IR을 합성합니다. -
10-16행
__builtin_add_overflow는 오버플로 발생 여부를 bool로 반환합니다. true이면__ubsan_handle_add_overflow런타임 핸들러가 호출됩니다. 핸들러는lib/ubsan.c에 구현되어 있으며 콜 스택과 값을printk로 출력합니다. -
24-27행
고정 크기 배열은 컴파일러가 상한을 알 수 있으므로
CONFIG_UBSAN_BOUNDS가 런타임 비교를 삽입합니다.struct에__counted_by어트리뷰트가 붙은 가변 배열(flex array)도 동일하게 보호됩니다.
런타임 핸들러 레퍼런스
UBSAN이 삽입하는 런타임 핸들러는 모두 lib/ubsan.c에 정의되어 있습니다.
리포트 첫 줄의 함수 이름으로 어떤 UB인지 바로 판독할 수 있습니다.
| 핸들러 | CONFIG | 탐지 내용 |
|---|---|---|
__ubsan_handle_add_overflow | INTEGER_WRAP | 덧셈 오버플로 |
__ubsan_handle_sub_overflow | INTEGER_WRAP | 뺄셈 오버플로 |
__ubsan_handle_mul_overflow | INTEGER_WRAP | 곱셈 오버플로 |
__ubsan_handle_negate_overflow | INTEGER_WRAP | -INT_MIN |
__ubsan_handle_divrem_overflow | DIV_ZERO | 0 나누기 · INT_MIN / -1 |
__ubsan_handle_out_of_bounds | BOUNDS / ARRAY_BOUNDS / LOCAL_BOUNDS | 배열 범위 초과 접근 |
__ubsan_handle_shift_out_of_bounds | SHIFT | 시프트 양이 타입 비트 폭 초과 |
__ubsan_handle_load_invalid_value | BOOL / ENUM | bool/enum에 허용 외 값 로드 |
__ubsan_handle_implicit_conversion | (Clang 기본) | signed/unsigned truncation·conversion 탐지 |
__ubsan_handle_type_mismatch | (구버전) | 과거 포맷의 타입 불일치 — v1로 대체 |
__ubsan_handle_type_mismatch_v1 | ALIGNMENT / 기본 | NULL 역참조 · 정렬 불일치 · 객체 크기 불일치 |
__ubsan_handle_alignment_assumption | ALIGNMENT | __builtin_assume_aligned 위반 |
__ubsan_handle_builtin_unreachable | UNREACHABLE | __builtin_unreachable() 실행 |
FORTIFY_SOURCE와의 관계
CONFIG_FORTIFY_SOURCE는 memcpy·strcpy 같은 문자열/메모리 함수에 대해
컴파일 타임에 상수 크기를 추론해 경계를 검증합니다. 하지만 인덱스가 런타임 변수이면 FORTIFY가 아무것도 하지 못합니다.
UBSAN_BOUNDS는 이 공백을 메워 런타임 인덱스를 체크합니다. 커널 하드닝 관점에서 둘은 상호 보완적이므로,
많은 배포판 커널이 둘 다 켠 상태로 배포됩니다.
- FORTIFY_SOURCE — 컴파일 타임 + 상수 크기. 빌드 실패/경고로 잡음. 제로 런타임 비용
- UBSAN_BOUNDS — 런타임 + 동적 인덱스. fault 발생 시 리포트. 약간의 비용
- KASAN — 런타임 + 모든 바이트. Slab/Stack/Global 전체. 높은 비용
리포트 해석
================================================================================
UBSAN: array-index-out-of-bounds in drivers/net/example.c:42:15
index 10 is out of range for type 'int [10]'
CPU: 1 PID: 1234 Comm: test Not tainted 6.8.0 #1
Call trace:
dump_backtrace+0x0/0x1e0
ubsan_epilogue+0x10/0x50
__ubsan_handle_out_of_bounds+0x68/0x80
test_bounds+0x38/0x50
example_ioctl+0x20/0x40
================================================================================
UBSAN: signed-integer-overflow in fs/ext4/resize.c:123:30
2147483647 + 1 cannot be represented in type 'int'
================================================================================
UBSAN: shift-out-of-bounds in lib/test.c:88:14
shift exponent 33 is too large for 32-bit type 'int'
================================================================================
UBSAN: object-size-mismatch in fs/xfs/xfs_dir2.c:201:5
load of address 0x... with insufficient space for an object of type 'struct foo'
================================================================================
리포트의 첫 줄이 탐지 유형을 바로 알려 주므로, 핸들러 목록과 대조하면 UB 종류를 즉시 판정할 수 있습니다.
CPU·PID·Comm 라인은 발생한 태스크(Task) 문맥이고, Call trace는
UB를 유발한 호출 경로입니다.
실전 사례: UB가 만든 보안 문제
UBSAN이 탐지하는 UB는 "그냥 이상한 값"이 아니라 권한 경계·메모리 안전·커널 하드닝을 무너뜨리는 결함의 원인이 되는 경우가 많습니다. 대표적인 패턴 세 가지를 살펴봅니다.
사례 A: 크기 계산의 부호 있는 정수 오버플로 → 힙 OOB
/* 취약 코드 (간략화) */
struct msg *alloc_msg(int nr, int payload)
{
int total = nr * payload; /* (1) UB: 부호 있는 * 오버플로 */
return kmalloc(sizeof(*p) + total, GFP_KERNEL);
}
/*
* (1)에서 nr=0x10000, payload=0x10000이면 total이 음수로 래핑되고,
* kmalloc은 (small size)로 해석해 작은 버퍼를 돌려줍니다.
* 호출자가 nr*payload 바이트를 채우면 힙 OOB 발생.
*
* UBSAN_INTEGER_WRAP이 곱셈 오버플로를 리포트하고, FORTIFY_SOURCE + __counted_by로
* 후속 기록도 탐지됩니다. 수정은 check_mul_overflow() 또는 size_mul() 사용.
*/
사례 B: 시프트 오버플로로 의도치 않은 비트 마스크
/* 32비트 권한 비트마스크를 구성하려다 시프트 오버플로 */
u32 perm_mask(int bits)
{
return (1 << bits) - 1; /* bits>=32면 UB */
}
/* UBSAN_SHIFT 리포트:
* "shift exponent 32 is too large for 32-bit type 'int'"
* 결과 값이 아키텍처별로 달라지면서 권한 체크를 우회할 수 있음.
* 수정: bits를 BUILD_BUG_ON 또는 명시적 범위 체크로 제한. */
사례 C: 제어 흐름을 결정하는 bool 오염
/* 드라이버가 DMA·외부 장치에서 받은 바이트를 bool로 로드 */
struct ctrl {
bool secure; /* 0/1만 허용, 그 외는 UB */
u8 raw;
};
void handle(struct ctrl *c)
{
if (c->secure) /* raw가 0x42라면 true가 보장되지 않음 */
grant_access();
}
/* UBSAN_BOOL: "load of value ... is not a valid value for type '_Bool'"
* 하드닝 관점에서 외부 입력 bool은 반드시 정수로 받아 ==/!= 로 판정하세요. */
panic_on_warn vs UBSAN_TRAP
| 옵션 | 동작 | 영향 범위 | 권장 환경 |
|---|---|---|---|
CONFIG_UBSAN_TRAP=n (기본) |
UB 발견 시 printk 리포트 후 계속 실행 |
UBSAN 경고만 | 개발·CI (리포트 수집) |
CONFIG_UBSAN_TRAP=y |
UB 발견 즉시 ud2/brk 인스트럭션 → oops |
UBSAN만 | Android, 하드닝된 배포판 |
panic_on_warn=1 (부팅 파라미터) |
WARN()/WARN_ON()과 Sanitizer 경고 전반 → panic |
모든 WARN·Sanitizer | 퍼징·재현 자동 수집 |
panic_on_warn=1 + UBSAN_TRAP=y |
UB는 trap(oops), 다른 WARN은 panic | 경로별 분리 | 엄격 CI |
Android 커널은 Clang의 UBSan minimal runtime(ubsan_minimal.o)을 사용해
런타임 크기를 크게 줄이고 UBSAN_TRAP=y를 기본으로 켭니다. 공격자가 UB를 익스플로잇하기
위한 중간 단계로 활용하지 못하도록 즉시 oops를 내는 전략입니다.
의도적 UB 예외 처리
커널에는 "의도적으로 래핑(Wrap) 산술을 쓰는" 코드가 있습니다(해시(Hash), 체크섬(Checksum), 링 버퍼(Ring Buffer) 인덱스 등). 최근 커널은 이런 의도를 명시적으로 표기하는 방향으로 이동했습니다.
| 방법 | 적용 범위 | 용도 |
|---|---|---|
wrapping_add(type, a, b) / wrapping_sub(type, a, b) / wrapping_mul(type, a, b) |
표현식 단위 | 래핑 의도 명시. UBSAN이 무시 |
__attribute__((no_sanitize("signed-integer-overflow"))) |
함수 단위 | 특정 핸들러 종류만 제외 |
UBSAN_SANITIZE := n |
Makefile / 디렉토리 | 해당 오브젝트 파일 전체 계측 제외 |
UBSAN_SANITIZE_<file>.o := n |
파일 단위 | 단일 파일만 계측 제외 |
__counted_by(len) |
flex array 필드 | 구조체(Struct) 끝 가변 배열 길이를 정적으로 연결 (GCC 15+ / Clang 18+ 필요, CONFIG_CC_HAS_COUNTED_BY에서 자동 감지) |
/* 래핑 의도가 있는 해시 코드 예시 */
static u32 mix(u32 a, u32 b)
{
return wrapping_mul(u32, a, 0x85ebca6bU) ^
wrapping_add(u32, b, 0xc2b2ae35U);
}
/* flex array를 UBSAN이 인식하도록 __counted_by 사용 */
struct pkt {
u16 len;
u8 data[] __counted_by(len); /* UBSAN이 len 기준으로 bounds 검증 */
};
ARM64/RISC-V 정렬 차이
CONFIG_UBSAN_ALIGNMENT의 유용성은 아키텍처마다 다릅니다.
x86_64는 하드웨어가 비정렬 접근을 허용하지만, ARM64는 LDR 계열 일부와 atomic 연산이,
RISC-V는 대부분의 확장 연산이 정렬 요구를 가집니다. 이들 아키텍처에서는 UBSAN_ALIGNMENT가
잠재적 크래시를 예방하는 의미가 있으며, get_unaligned_*()/put_unaligned_*()
API를 써 의도적으로 비정렬 접근을 하는 경로는 UBSAN이 무시합니다.
운영 전략
UBSAN은 오버헤드가 상대적으로 낮아 프로덕션에서도 일부 옵션(예: CONFIG_UBSAN_BOUNDS)을
활성화해 두는 것이 권장됩니다. 다만 CONFIG_UBSAN_TRAP=y는 UB 발견 즉시 커널을 중단시키므로
리포트만 남기려면 n으로 두어야 합니다.
- 개발/CI —
CONFIG_UBSAN=y의 모든 세부 옵션 활성화,panic_on_warn=1로 자동 수집 - 프로덕션 —
CONFIG_UBSAN_BOUNDS,CONFIG_UBSAN_SHIFT,CONFIG_UBSAN_BOOL정도만 유지 - Android/배포판 —
CONFIG_UBSAN_TRAP=y로 즉시 oops (공격 억제 효과) - 의도적인 래핑(wrap) 산술이 필요한 코드는
wrapping_add/no_sanitize속성으로 예외 처리
흔한 실수와 디버깅 팁
- 빌드 실패:
missing __ubsan_handle_*—CONFIG_UBSAN=y로 컴파일했는데 런타임이 링크 안 된 상황.lib/ubsan.c가 Makefile에 포함됐는지 확인하세요. - 같은 리포트가 무한 반복 — 커널 파라미터
ubsan_once=1로 사이트당 1회만 리포트하도록 할 수 있습니다 (Android 기본값). - Linux 6.x의
-fno-strict-overflow— 커널은 기본적으로 이 플래그를 써서 signed overflow를 "wraparound" 의미로 취급합니다.UBSAN_INTEGER_WRAP은 그럼에도 불구하고 발견 즉시 리포트합니다. - flex array 관련 false positive —
__counted_by를 붙이거나 구조체 끝 배열을flexible array로 선언하세요. 고정 크기 1로 선언된 "fake array"는 bounds 체크가 오작동합니다. - 모듈만 계측 —
UBSAN_SANITIZE := y를 모듈 디렉토리 Makefile에 추가하면 해당 모듈만 UBSAN이 적용됩니다.
커널 설정
# 모든 UBSAN 옵션 활성화 (개발/CI)
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y
CONFIG_UBSAN_INTEGER_WRAP=y # 정수 오버플로 (부호 있음/없음 통합)
CONFIG_UBSAN_SHIFT=y
CONFIG_UBSAN_DIV_ZERO=y
CONFIG_UBSAN_BOOL=y
CONFIG_UBSAN_ENUM=y
CONFIG_UBSAN_ALIGNMENT=y
CONFIG_UBSAN_UNREACHABLE=y # __builtin_unreachable 실행 탐지
CONFIG_UBSAN_BOUNDS_STRICT=y # GCC 엄격 모드 (GCC 빌드 시)
CONFIG_UBSAN_ARRAY_BOUNDS=y # Clang array bounds (Clang 빌드 시)
CONFIG_UBSAN_LOCAL_BOUNDS=y # Clang 로컬 동적 bounds
CONFIG_UBSAN_TRAP=n # 리포트만 (권장)
# 프로덕션 하드닝 (선별적)
CONFIG_UBSAN=y
CONFIG_UBSAN_BOUNDS=y
CONFIG_UBSAN_SHIFT=y
CONFIG_UBSAN_BOOL=y
CONFIG_UBSAN_TRAP=y # Android 스타일 즉시 oops
참고자료
- UndefinedBehaviorSanitizer (UBSAN) — 커널 공식 문서
- Clang UndefinedBehaviorSanitizer
- GCC Instrumentation Options (-fsanitize=undefined)
- LWN: Wrap-around arithmetic and the kernel
- 소스 코드:
lib/ubsan.c,include/linux/ubsan.h,scripts/Makefile.ubsan