Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 34.326 phút đọc

UndefinedBehaviorSanitizer

Sau bài này bạn sẽ làm được

  • Dịch và chạy với -fsanitize=undefined
  • Đọc thông báo UBSan và tra về đúng dòng mã
  • Bật -fno-sanitize-recover để dừng ngay ở lỗi đầu tiên
  • Kết hợp UBSan với ASan trong một lần dịch

UBSan gần như miễn phí: chậm thêm khoảng hai mươi phần trăm. Nó bắt đúng nhóm lỗi mà ASan không thấy và trình biên dịch không cảnh báo được, tức là tràn số, dịch bit sai và chia cho không.

#Chạy thử

ub.c
#include <stdio.h>
#include <limits.h>

__attribute__((noinline)) int cong(int a, int b) { return a + b; }
__attribute__((noinline)) unsigned dich(unsigned x, int n) { return x << n; }
__attribute__((noinline)) int chia(int a, int b) { return a / b; }

int main(void) {
    printf("cong  = %d\n", cong(INT_MAX, 1));
    printf("dich  = %u\n", dich(1, 32));
    printf("chia  = %d\n", chia(10, 0));

    return 0;
}
terminal
# Không có UBSan: hai dòng đầu chạy êm, dòng ba mới sập
gcc -std=c11 -g -O1 -Wall -Wextra -o ub ub.c && ./ub
cong  = -2147483648
dich  = 1
Floating point exception (core dumped)
terminal
# Có UBSan
gcc -std=c11 -g -O1 -fsanitize=undefined -o ub ub.c && ./ub
ub.c:4:44: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
cong  = -2147483648
ub.c:5:56: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
dich  = 1
ub.c:6:44: runtime error: division by zero
Floating point exception (core dumped)

#Mười kiểm tra hay dùng

Tên nhómBắt gìVí dụ
signed-integer-overflowTràn số có dấuINT_MAX + 1
shiftDịch quá độ rộng, dịch số âm1u << 32
integer-divide-by-zeroChia số nguyên cho khôngx / 0
nullGiải tham chiếu con trỏ NULL*(int *)0
alignmentCon trỏ lệch căn chỉnh*(int *)(buf + 1)
boundsNgoài biên mảng khi biết kích thướcint a[5]; a[i]
object-sizeGhi vượt kích thước đối tượngmemcpy quá lớn
float-cast-overflowÉp số thực sang nguyên bị tràn(int)1e20
returnHàm không void thiếu returnBài 33.3 mục 14
vla-boundKích thước VLA âm hoặc bằng khôngint a[n] với n = 0
unreachableChạy vào __builtin_unreachableNhánh bạn thề không xảy ra
pointer-overflowTính địa chỉ bị trànp + n quá lớn

Ba kiểm tra đáng chú ý nhất

/* 1. alignment: quan trong tren ARM, noi truy cap lech lam sap chuong trinh */

char bo_dem[16];
int *p = (int *)(bo_dem + 1);      /* lech 1 byte */
*p = 42;

/* UBSan: runtime error: store to misaligned address 0x... for type 'int',
   which requires 4 byte alignment */

/* 2. bounds: chi hoat dong khi kich thuoc BIET LUC DICH */

int a[5];
int i = doc_so();
a[i] = 1;                          /* UBSan bat neu i ngoai 0..4 */

int *b = malloc(5 * sizeof *b);
b[i] = 1;                          /* UBSan KHONG bat: kich thuoc khong biet.
                                      Cai nay la viec cua ASan. */

/* 3. return: bat duoc dung cho ma UB o Bai 33.2 */

int f(int x) { if (x) return 1; }

/* UBSan: runtime error: execution reached the end of a value-returning
   function without returning a value */

#Chọn nhóm kiểm tra

Bốn cách gọi
# 1. Tat ca cac kiem tra mac dinh
gcc -fsanitize=undefined prog.c

# 2. Chi mot vai nhom
gcc -fsanitize=signed-integer-overflow,shift,null prog.c

# 3. Tat ca tru mot nhom
gcc -fsanitize=undefined -fno-sanitize=alignment prog.c

# 4. Them nhom KHONG nam trong "undefined"
gcc -fsanitize=undefined,unsigned-integer-overflow prog.c
NhómCó trong GCCCó trong ClangGhi chú
undefined (nhóm chính)CóCóDùng cái này
unsigned-integer-overflowKhôngCóKhông phải UB, hay báo giả
implicit-conversionKhôngCóBắt ép kiểu ngầm mất dữ liệu
local-boundsKhôngCóMảng cục bộ, cần -O
nullabilityKhôngCóDùng với _Nonnull
float-divide-by-zeroCóCóKhông phải UB nếu theo IEEE 754

#Dừng ngay hay chạy tiếp

# Mac dinh: bao roi chay tiep
gcc -fsanitize=undefined prog.c

# Dung han o loi dau tien, va tra ve ma thoat khac 0
gcc -fsanitize=undefined -fno-sanitize-recover=all prog.c

# Hoac dat luc chay
UBSAN_OPTIONS=halt_on_error=1 ./prog

# In ca vet goi cho moi bao loi
UBSAN_OPTIONS=print_stacktrace=1 ./prog
terminal
gcc -std=c11 -g -O1 -fsanitize=undefined -fno-sanitize-recover=all -o ub ub.c && ./ub
ub.c:4:44: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
echo $?
1

#Kết hợp với ASan

gcc -std=c11 -g3 -O1 \
    -fno-omit-frame-pointer \
    -fsanitize=address,undefined \
    -fno-sanitize-recover=all \
    -Wall -Wextra \
    -o prog prog.c

# Hai sanitizer nay dung chung duoc, va nen dung chung.
# Chi phi cong lai van chi khoang 2.2 lan.

# KHONG dung chung voi thread:
gcc -fsanitize=address,thread prog.c
# error: -fsanitize=address is incompatible with -fsanitize=thread
CặpDùng chung đượcGhi chú
address + undefinedCóCặp nên dùng mặc định
address + leakCóLeak đã nằm sẵn trong address
thread + undefinedCóLượt chạy thứ hai
address + threadKhôngLỗi ngay lúc dịch
address + memoryKhôngLỗi ngay lúc dịch
memory + undefinedCóChỉ Clang, và cần dịch cả thư viện

#Giới hạn

Tự làm thử

  1. Viết chương trình có ba lỗi UB rồi chạy có và không có UBSan.
  2. Xác nhận mã thoát là 0 nếu không dùng -fno-sanitize-recover.
  3. Thử kiểm tra alignment bằng cách ép con trỏ lệch một byte.
  4. Chứng minh bounds bắt được mảng cố định còn ASan thì không.
  5. Bật unsigned-integer-overflow trên một dự án có hàm băm và đếm số báo giả.
  6. Viết Makefile ba lượt và chạy nó trên một dự án của bạn.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • UBSan chỉ chậm thêm khoảng hai mươi phần trăm và bắt đúng nhóm lỗi ASan không thấy.
  • Báo lỗi nói cả phép tính lẫn kiểu: 2147483647 + 1 cannot be represented in type 'int'.
  • Mặc định nó báo rồi chạy tiếp và trả về mã thoát 0, nên trong CI phải dùng -fno-sanitize-recover=all.
  • bounds bắt được mảng biết kích thước lúc dịch, chỗ mà ASan bỏ qua, nên hai công cụ bổ sung cho nhau.
  • Nó không bắt strict aliasing, không bắt biến chưa khởi tạo, và chỉ thấy đường mã thật sự chạy.