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.
gcc -fsanitize=undefined -g -O0 -std=c11 program.c -o programKita 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.
/* 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;
}gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan1.c -o /tmp/test_ubsan1
/tmp/test_ubsan1Output yang Dihasilkan:
/tmp/test_ubsan1.c:6:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
c = -2147483648Mari kita bedah elemen kunci laporan UBSan di atas:
/tmp/test_ubsan1.c:6:9: lokasi sumber UB—file, baris 6, kolom 9. Baris 6 adalahint c = a + b;dan kolom 9 adalah posisi tanda+dalam ekspresi.runtime error: signed integer overflow: jenis UB yang terdeteksi. UBSan menggunakan istilahruntime error(bukanerror) karena UB tidak menghentikan program secara default—program berlanjut dengan hasil yang implementation-defined.2147483647 + 1 cannot be represented in type 'int': UBSan mencetak ekspresi konkret yang meluap beserta tipe data terlibat (int). NilaiINT_MAXadalah2147483647, ditambah1menghasilkan2147483648yang tidak muat dalamint(max2147483647).c = -2147483648: program tetap mencetak hasil wrap-around (INT_MIN). UBSan tidak menghentikan program kecuali kita menambahkan opsihalt_on_error=1.
Deteksi Shift Out-of-Range #
Operasi shift << dan >> memiliki batasan ketat yang sering dilanggar:
- Shift kiri
a << bdengan hasil yang tidak muat dalam tipe signed adalah UB. - Shift dengan jumlah >= width tipe (misalnya
1 << 32padaint32-bit) adalah UB. - Shift dengan jumlah negatif juga UB.
/* 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;
}gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan2.c -o /tmp/test_ubsan2
/tmp/test_ubsan2Output yang Dihasilkan:
/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 = 0Mari kita uraikan dua laporan UB pada program shift di atas:
- Baris 4 (
x << 31): operasi1 << 31menghasilkan nilai2147483648yang tidak muat dalamintsigned (max2147483647). UBSan melaporkanleft shift of 1 by 31 places cannot be represented in type 'int'. Perbaikan yang benar adalah memakai tipeunsigned int:unsigned int x = 1; unsigned int shifted = x << 31;. - Baris 6 (
1 << 32): jumlah shift32sama dengan width tipeint(32 bit). Standar C menyatakan shift dengan jumlah >= width adalah UB. UBSan melaporkanshift exponent 32 is too large for 32-bit type 'int'. gcc juga menampilkan compile-time warning-Wshift-count-overflowkarena 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.
/* 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;
}gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan3.c -o /tmp/test_ubsan3
/tmp/test_ubsan3Output yang Dihasilkan:
/tmp/test_ubsan3.c:5:9: runtime error: member access within null pointer of type 'struct Data'
Segmentation faultUBSan 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.
/* 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;
}gcc -fsanitize=undefined -g -O0 -std=c11 /tmp/test_ubsan4.c -o /tmp/test_ubsan4
/tmp/test_ubsan4Output yang Dihasilkan:
/tmp/test_ubsan4.c:5:15: runtime error: division by zero
Floating point exceptionLaporan 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 Check | Yang Dideteksi |
|---|---|
-fsanitize=signed-integer-overflow | overflow pada signed int |
-fsanitize=shift | shift out-of-range |
-fsanitize=shift-base | shift operand negatif |
-fsanitize=shift-exponent | shift amount >= width |
-fsanitize=null | null pointer dereference |
-fsanitize=alignment | akses unaligned |
-fsanitize=bool | load bool invalid |
-fsanitize=bounds | akses array out-of-bounds (sebagian) |
-fsanitize=enum | load enum invalid |
-fsanitize=pointer-overflow | overflow pada pointer arithmetic |
-fsanitize=integer-divide-by-zero | pembagian 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:...:
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 /tmp/test_ubsan1| Variabel | Fungsi |
|---|---|
halt_on_error=1 | berhenti pada UB pertama (default: 0, lanjut) |
print_stacktrace=1 | cetak backtrace setelah laporan |
print_stats=1 | cetak statistik jumlah UB per kategori |
silence_unsigned_overflow=1 | jangan 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:
gcc -fsanitize=address,undefined -g -O0 -std=c11 program.c -o programKombinasi ini adalah standar yang disarankan untuk build development dan CI. Overhead gabungan sekitar 2.5x—masih sangat layak untuk dijalankan setiap commit.