UBSAN (Undefined Behavior Sanitizer)

UBSAN은 C 언어 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지하는 커널 Sanitizer입니다. 정수 오버플로, 배열 범위 초과, 시프트 오류, NULL 포인터 역참조 등 컴파일러 최적화(Compiler Optimization)에 의해 예측 불가능해지는 영역을 런타임 체크로 포착해, 보안 취약점(Vulnerability)으로 이어질 수 있는 케이스를 페이지(Page) 단위 로그로 남깁니다. 오버헤드(Overhead)가 낮아 프로덕션 디버깅(Debugging)에도 제한적으로 유지할 수 있습니다.

전제 조건: C 언어 & 커널 C 관용어의 정수 승격·정렬 규칙, GCC 가이드-fsanitize 플래그 구조를 알면 이해가 빠릅니다.
일상 비유: UB는 계약서의 공백 조항과 같습니다. C 표준이 "이 경우는 어떻게 동작하는지 정해두지 않았다"고 선언한 영역은 컴파일러가 임의로 해석할 수 있어, 어제는 잘 돌던 코드가 오늘 최적화 후 다른 동작을 할 수 있습니다. UBSAN은 계약서 공백에 해당하는 연산이 실제로 실행되는 순간 "여기 공백입니다"라고 로그를 남기는 감사 도구입니다.

핵심 요약

  • 런타임 탐지 — 컴파일러가 UB 발생 가능 지점에 체크 코드를 삽입, 실행 중 발견 시 리포트
  • 낮은 오버헤드 — 옵션별로 다르지만 수 % 이내. CONFIG_UBSAN_BOUNDS는 프로덕션 유지 가능
  • 세분화된 옵션 — 오버플로/범위/정렬/시프트/NULL 등을 개별 CONFIG로 토글
  • FORTIFY_SOURCE 보완 — 컴파일 타임 체크가 불가능한 동적 인덱스를 UBSAN이 런타임에 포착
  • 의도적 UB 표기 — 래핑 산술이 필요한 곳은 wrapping_add/no_sanitize로 제외 표기

단계별 이해

  1. 컴파일러 계측
    GCC/Clang이 각 UB 후보 연산 앞에 __builtin_*_overflow 체크 또는 범위 비교를 삽입합니다.
  2. 실행
    정상 실행 시 분기 예측(Branch Prediction) 덕분에 오버헤드가 무시할 만합니다.
  3. UB 발견
    체크가 실패하면 __ubsan_handle_* 런타임 핸들러(Handler)가 호출되며, NULL 역참조(Dereference) 같은 포인터 문제도 이 경로를 거칩니다.
  4. 리포트
    핸들러가 콜 스택·소스 위치·값을 printk로 출력합니다. CONFIG_UBSAN_TRAP=y면 즉시 oops.
  5. 대응
    버그면 수정, 의도적 UB면 wrapping_add/속성/UBSAN_SANITIZE := n으로 제외.

개요

UBSAN은 C 언어 표준에서 정의되지 않은 동작(Undefined Behavior, UB)을 런타임에 탐지합니다. UB는 컴파일러가 임의로 최적화할 수 있는 영역이므로 예측 불가능한 버그와 보안 취약점의 원인이 됩니다.

왜 UB가 위험한가: 부호 있는 정수 오버플로, 범위 초과 배열 접근, 비정렬 포인터 역참조 등은 표준상 "무엇이든 일어날 수 있음". 컴파일러는 UB가 발생하지 않는다고 가정하고 최적화하므로, UB를 유발한 코드는 디버그 빌드에서는 동작하다가 릴리스 빌드에서 갑자기 사라지거나 엉뚱한 값으로 바뀔 수 있습니다.
UBSAN 탐지 흐름 ① 원본 커널 소스 int r = a + b; arr[idx] = x; v = (1 << n); UB 가능 연산들 컴파일 ② 컴파일러 계측 삽입 if (__builtin_add_overflow(a,b,&r)) __ubsan_handle_add_overflow(...); if (idx >= size) __ubsan_handle_out_of_bounds(...); 실행 ③ 런타임 핸들러 lib/ubsan.c __ubsan_handle_*() → ubsan_prologue() → printk UBSAN 리포트 리포트와 옵션 제어 CONFIG_UBSAN_TRAP=n 리포트만 남기고 계속 실행 (기본, CI/프로덕션) CONFIG_UBSAN_TRAP=y 즉시 ud2/brk → oops (디버그·엄격 환경) panic_on_warn UBSAN 경고 시 panic (퍼징 fuzzer 탐지용) 주요 CONFIG 옵션 UBSAN_BOUNDS · UBSAN_SHIFT · UBSAN_INTEGER_WRAP · UBSAN_DIV_ZERO · UBSAN_BOOL · UBSAN_ENUM · UBSAN_ALIGNMENT · UBSAN_UNREACHABLE

탐지 유형과 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() 실행 시 중간
NULL 포인터 역참조·타입 불일치: 과거에는 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_overflowINTEGER_WRAP덧셈 오버플로
__ubsan_handle_sub_overflowINTEGER_WRAP뺄셈 오버플로
__ubsan_handle_mul_overflowINTEGER_WRAP곱셈 오버플로
__ubsan_handle_negate_overflowINTEGER_WRAP-INT_MIN
__ubsan_handle_divrem_overflowDIV_ZERO0 나누기 · INT_MIN / -1
__ubsan_handle_out_of_boundsBOUNDS / ARRAY_BOUNDS / LOCAL_BOUNDS배열 범위 초과 접근
__ubsan_handle_shift_out_of_boundsSHIFT시프트 양이 타입 비트 폭 초과
__ubsan_handle_load_invalid_valueBOOL / ENUMbool/enum에 허용 외 값 로드
__ubsan_handle_implicit_conversion(Clang 기본)signed/unsigned truncation·conversion 탐지
__ubsan_handle_type_mismatch(구버전)과거 포맷의 타입 불일치 — v1로 대체
__ubsan_handle_type_mismatch_v1ALIGNMENT / 기본NULL 역참조 · 정렬 불일치 · 객체 크기 불일치
__ubsan_handle_alignment_assumptionALIGNMENT__builtin_assume_aligned 위반
__ubsan_handle_builtin_unreachableUNREACHABLE__builtin_unreachable() 실행

FORTIFY_SOURCE와의 관계

CONFIG_FORTIFY_SOURCEmemcpy·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으로 두어야 합니다.

권장 조합:
  • 개발/CICONFIG_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 속성으로 예외 처리

흔한 실수와 디버깅 팁

커널 설정

# 모든 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

참고자료

다음 학습: