Standar C mendefinisikan sejumlah konstruksi sebagai undefined behavior (UB): operasi yang hasilnya tidak dijamin oleh standar sehingga compiler bebas mengasumsikan kode tersebut tidak pernah terjadi. Bug UB sangat berbahaya karena program tetap berjalan, terlihat benar pada kasus uji sederhana, tetapi menghasilkan hasil yang salah tak terduga pada input berbeda atau setelah perubahan compiler. UndefinedBehaviorSanitizer (UBSan) menginstrumentasi program saat kompilasi untuk mendeteksi berbagai kelas UB saat runtime: signed integer overflow, shift out-of-range, null pointer dereference, division by zero, dan masih banyak lagi. Pada chapter ini kita akan mengaktifkan UBSan dengan -fsanitize=undefined, menguji empat kelas UB umum, dan membaca laporan yang dihasilkan. Dokumentasi resmi UBSan tersedia di clang.llvm.org/docs/UndefinedBehaviorSanitizer.html.

Mengaktifkan UBSan #

Flag -fsanitize=undefined mengaktifkan sekumpulan check UB yang sudah dikurasi. gcc dan clang keduanya mendukung flag ini. UBSan menambahkan overhead yang jauh lebih kecil dibanding ASan—hanya sekitar 10-20% perlambatan—sehingga bisa dipakai pada build produksi yang membutuhkan pemeriksaan tambahan.

bash
gcc -fsanitize=undefined -g -O0 -std=c11 program.c -o program

Kita juga bisa mengaktifkan UBSan bersama ASan dengan satu perintah: gcc -fsanitize=address,undefined. Kombinasi ini adalah default yang disarankan untuk build development.

Deteksi Signed Integer Overflow #

Operasi aritmatika pada signed integer yang melebihi rentang tipe adalah UB. Contoh klasik: INT_MAX + 1 tidak terdefinisi oleh standar C. Banyak compiler menghasilkan INT_MIN (wrap-around) untuk kasus ini, tetapi optimizer gcc bisa saja menghapus branch yang seharusnya menangani overflow karena compiler diasumsikan UB tidak pernah terjadi.

c
/* File: /tmp/test_ubsan1.c */
#include <stdio.h>
#include <limits.h>
int main(void) {
    int a = INT_MAX;
    int b = 1;
    int c = a + b;  /* signed overflow = UB */
    printf("c = %d\n", c);
    return 0;
}
bash
gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan1.c -o /tmp/test_ubsan1
/tmp/test_ubsan1

Output yang Dihasilkan:

text
/tmp/test_ubsan1.c:6:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
c = -2147483648

Mari kita bedah elemen kunci laporan UBSan di atas:

  1. /tmp/test_ubsan1.c:6:9: lokasi sumber UB—file, baris 6, kolom 9. Baris 6 adalah int c = a + b; dan kolom 9 adalah posisi tanda + dalam ekspresi.
  2. runtime error: signed integer overflow: jenis UB yang terdeteksi. UBSan menggunakan istilah runtime error (bukan error) karena UB tidak menghentikan program secara default—program berlanjut dengan hasil yang implementation-defined.
  3. 2147483647 + 1 cannot be represented in type 'int': UBSan mencetak ekspresi konkret yang meluap beserta tipe data terlibat (int). Nilai INT_MAX adalah 2147483647, ditambah 1 menghasilkan 2147483648 yang tidak muat dalam int (max 2147483647).
  4. c = -2147483648: program tetap mencetak hasil wrap-around (INT_MIN). UBSan tidak menghentikan program kecuali kita menambahkan opsi halt_on_error=1.

Deteksi Shift Out-of-Range #

Operasi shift << dan >> memiliki batasan ketat yang sering dilanggar:

  • Shift kiri a << b dengan hasil yang tidak muat dalam tipe signed adalah UB.
  • Shift dengan jumlah >= width tipe (misalnya 1 << 32 pada int 32-bit) adalah UB.
  • Shift dengan jumlah negatif juga UB.
c
/* File: /tmp/test_ubsan2.c */
#include <stdio.h>
int main(void) {
    int x = 1;
    int shifted = x << 31;  /* shifting into sign bit = UB for int */
    printf("shifted = %d\n", shifted);
    int y = 1 << 32;  /* shift >= width of type = UB */
    printf("y = %d\n", y);
    return 0;
}
bash
gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan2.c -o /tmp/test_ubsan2
/tmp/test_ubsan2

Output yang Dihasilkan:

text
/tmp/test_ubsan2.c:4:21: runtime error: left shift of 1 by 31 places cannot be represented in type 'int'
/tmp/test_ubsan2.c:6:15: runtime error: shift exponent 32 is too large for 32-bit type 'int'
shifted = -2147483648
y = 0

Mari kita uraikan dua laporan UB pada program shift di atas:

  1. Baris 4 (x << 31): operasi 1 << 31 menghasilkan nilai 2147483648 yang tidak muat dalam int signed (max 2147483647). UBSan melaporkan left shift of 1 by 31 places cannot be represented in type 'int'. Perbaikan yang benar adalah memakai tipe unsigned int: unsigned int x = 1; unsigned int shifted = x << 31;.
  2. Baris 6 (1 << 32): jumlah shift 32 sama dengan width tipe int (32 bit). Standar C menyatakan shift dengan jumlah >= width adalah UB. UBSan melaporkan shift exponent 32 is too large for 32-bit type 'int'. gcc juga menampilkan compile-time warning -Wshift-count-overflow karena operasi shift ini bisa dievaluasi pada saat kompilasi.

Perhatikan bahwa gcc tetap menghasilkan binary dan program tetap mencetak nilai (shifted = -2147483648, y = 0)—hasil ini implementation-defined, bukan jaminan standar.

Deteksi Null Pointer Dereference #

Akses ke field struct melalui pointer NULL adalah UB. UBSan bisa mendeteksi kasus ini sebelum benar-benar menyebabkan segfault, memberikan laporan yang lebih informatif daripada crash mentah.

c
/* File: /tmp/test_ubsan3.c */
#include <stdio.h>
typedef struct { int nilai; } Data;
int main(void) {
    Data *d = (Data *)0;
    int v = d->nilai;  /* null member access = UB */
    printf("v = %d\n", v);
    return 0;
}
bash
gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan3.c -o /tmp/test_ubsan3
/tmp/test_ubsan3

Output yang Dihasilkan:

text
/tmp/test_ubsan3.c:5:9: runtime error: member access within null pointer of type 'struct Data'
Segmentation fault

UBSan melaporkan member access within null pointer of type 'struct Data' pada baris 5 (int v = d->nilai;). Setelah laporan UBSan, program tetap melakukan dereferensi dan crash dengan Segmentation fault. UBSan tidak menghentikan program sebelum dereferensi terjadi—ia hanya melaporkan; perilaku dereferensi sendiri tetap menjadi UB.

Deteksi Division by Zero #

Pembagian integer dengan nol adalah UB. Tanpa UBSan, program crash dengan sinyal SIGFPE (Floating Point Exception—nama historis untuk operasi arithmetic ilegal) tanpa backtrace. UBSan melaporkan lokasi sebelum crash terjadi.

c
/* File: /tmp/test_ubsan4.c */
#include <stdio.h>
int main(void) {
    int a = 10;
    int b = 0;
    int c = a / b;  /* division by zero = UB */
    printf("c = %d\n", c);
    return 0;
}
bash
gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan4.c -o /tmp/test_ubsan4
/tmp/test_ubsan4

Output yang Dihasilkan:

text
/tmp/test_ubsan4.c:5:15: runtime error: division by zero
Floating point exception

Laporan UBSan menunjukkan baris 5 kolom 15—tepat posisi operator / pada ekspresi a / b. Setelah laporan, kernel mengirim SIGFPE yang menyebabkan program berakhir dengan pesan shell Floating point exception. Tanpa UBSan, kita hanya akan melihat Floating point exception tanpa petunjuk baris sumber.

Subcheck UBSan yang Tersedia #

Flag -fsanitize=undefined adalah singkatan untuk sekumpulan check individual. Berikut beberapa check yang paling sering dipakai:

Flag CheckYang Dideteksi
-fsanitize=signed-integer-overflowoverflow pada signed int
-fsanitize=shiftshift out-of-range
-fsanitize=shift-baseshift operand negatif
-fsanitize=shift-exponentshift amount >= width
-fsanitize=nullnull pointer dereference
-fsanitize=alignmentakses unaligned
-fsanitize=boolload bool invalid
-fsanitize=boundsakses array out-of-bounds (sebagian)
-fsanitize=enumload enum invalid
-fsanitize=pointer-overflowoverflow pada pointer arithmetic
-fsanitize=integer-divide-by-zeropembagian dengan nol

Kita bisa memilih subset check dengan menggabungkan beberapa flag. Misalnya untuk fokus pada bug aritmatika: gcc -fsanitize=signed-integer-overflow,shift,integer-divide-by-zero program.c -o program.

Variabel Lingkungan UBSan #

UBSan dikendalikan oleh variabel lingkungan UBSAN_OPTIONS dengan format key=value:key=value:...:

bash
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 /tmp/test_ubsan1
VariabelFungsi
halt_on_error=1berhenti pada UB pertama (default: 0, lanjut)
print_stacktrace=1cetak backtrace setelah laporan
print_stats=1cetak statistik jumlah UB per kategori
silence_unsigned_overflow=1jangan laporkan unsigned overflow (default: 0)

Menggabungkan ASan dan UBSan #

ASan dan UBSan saling melengkapi: ASan fokus pada bug memori (overflow, use-after-free), UBSan fokus pada bug semantik (overflow aritmatika, UB shift). Keduanya bisa diaktifkan bersamaan dengan satu perintah:

bash
gcc -fsanitize=address,undefined -g -O0 -std=c11 program.c -o program

Kombinasi ini adalah standar yang disarankan untuk build development dan CI. Overhead gabungan sekitar 2.5x—masih sangat layak untuk dijalankan setiap commit.