Bug memori seperti heap-buffer-overflow, use-after-free, atau signed integer overflow sering tidak menampilkan gejala langsung. Program berjalan normal pada input kecil, lalu crash misterius di produksi dengan data yang tidak terduga. Sanitizer adalah alat GCC dan Clang yang menambahkan pemeriksaan runtime ke setiap akses memori dan operasi aritmatika. Pada chapter ini kita memakai AddressSanitizer (ASan) untuk mendeteksi bug memori, UndefinedBehaviorSanitizer (UBSan) untuk mendeteksi undefined behavior, dan menjalankan test suite dengan kombinasi keduanya.

Tiga Sanitizer Utama #

GCC dan Clang menyediakan tiga sanitizer utama yang sering dipakai bersamaan:

  • ASan (-fsanitize=address): Mendeteksi heap-buffer-overflow, stack-buffer-overflow, use-after-free, double-free, dan memory leak. Setiap malloc/free dipantau; setiap akses memori diperiksa apakah berada dalam wilayah yang valid.
  • UBSan (-fsanitize=undefined): Mendeteksi signed integer overflow, null pointer dereference, division by zero, misaligned pointer, dan banyak undefined behavior lainnya. UBSan dapat di-set untuk continue (mencatat saja) atau abort pada pelanggaran.
  • LSan (-fsanitize=leak): Mendeteksi memory leak dengan menjalankan pemeriksaan leak saat program exit. LSan otomatis aktif bila ASan digunakan.

Flag -fsanitize=address,undefined menggabungkan ASan dan UBSan dalam satu build—pilihan yang direkomendasikan untuk test suite.

Build Bersih dengan ASan + UBSan #

Sebagai contoh, kita mulai dengan program yang benar—alokasi array int ukuran 5, mengisi indeks 0 hingga 4, menjumlahkan, lalu membebaskan memori. Build dengan ASan dan UBSan seharusnya tidak menghasilkan error.

c
/* File: example.c — contoh kode aman */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int *make_array(size_t n) {
    int *p = malloc(n * sizeof(int));
    if (p == NULL) return NULL;
    for (size_t i = 0; i < n; i++) p[i] = (int)(i * 2);
    return p;
}

int main(void) {
    int *arr = make_array(5);
    if (arr == NULL) return 1;
    int sum = 0;
    for (size_t i = 0; i < 5; i++) sum += arr[i];
    printf("sum = %d\n", sum);
    free(arr);
    return 0;
}

Build dengan sanitizer dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -o example example.c && ./example

Output yang Dihasilkan:

text
sum = 20

Program berjalan bersih—tidak ada pelanggaran terdeteksi. Output sum = 20 adalah penjumlahan 0 + 2 + 4 + 6 + 8.

Deteksi Heap-Buffer-Overflow #

Sekarang kita sengaja menulis bug: akses arr[5] padahal array hanya memiliki 5 elemen (index 0 hingga 4). Tanpa ASan, bug ini mungkin tidak crash—data tetap tertulis ke lokasi setelah array. ASan menandai redzone di sekitar setiap alokasi sehingga akses di luar batas langsung memicu error.

c
/* File: heap_overflow.c — BUG: write ke arr[5] padahal hanya 5 elemen */
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *arr = malloc(5 * sizeof(int));
    if (arr == NULL) return 1;
    for (int i = 0; i < 5; i++) arr[i] = i;
    arr[5] = 99;  /* BUG: out-of-bounds write */
    printf("arr[5] = %d\n", arr[5]);
    free(arr);
    return 0;
}

Build dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address -o heap_overflow heap_overflow.c && ./heap_overflow

Output yang Dihasilkan (dipotong untuk ringkas):

text
=================================================================
==9849==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000054
WRITE of size 4 at 0x503000000054 thread T0
    #0 0x... in main (/tmp/test_sanitizers/heap_overflow+0x1282)
    #1 0x... in __libc_start_main
    #2 0x... in _start

0x503000000054 is located 0 bytes after 20-byte region [0x503000000040,0x503000000054)
allocated by thread T0 here:
    #0 0x... in malloc

ASan menunjuk WRITE of size 4 pada alamat 0x503000000054—tepat 0 bytes after wilayah 20-byte (5 * sizeof(int)). Pesan ini langsung mengidentifikasi bug: kita menulis satu int di luar batas array.

Deteksi Use-After-Free #

Bug use-after-free muncul saat program mengakses pointer setelah free. Tanpa ASan, akses ini mungkin mengembalikan nilai lama yang kebetulan masih utuh—bug tersembunyi hingga kondisi alokator berubah. ASan menandai wilayah yang sudah di-free sebagai poisoned, sehingga akses berikutnya langsung memicu error.

c
/* File: use_after_free.c — BUG: arr diakses setelah free */
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *arr = malloc(5 * sizeof(int));
    if (arr == NULL) return 1;
    arr[0] = 42;
    free(arr);
    printf("arr[0] = %d\n", arr[0]);  /* BUG: use-after-free */
    return 0;
}

Build dengan ASan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address -o use_after_free use_after_free.c && ./use_after_free

Output yang Dihasilkan (dipotong untuk ringkas):

text
=================================================================
==9866==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000000040
READ of size 4 at 0x503000000040 thread T0
    #0 0x... in main (/tmp/test_sanitizers/use_after_free+0x1263)
    #1 0x... in __libc_start_main

0x503000000040 is located 0 bytes inside of 20-byte region [0x503000000040,0x503000000054)
freed by thread T0 here:
    #0 0x... in free

ASan memberi tahu bahwa alamat yang diakses sudah di-free sebelumnya—bahkan menampilkan backtrace lokasi free yang membebaskan memori tersebut. Informasi ini sangat berharga untuk debugging.

Deteksi Signed Overflow dan Null Deref dengan UBSan #

UBSan mendeteksi kelas bug berbeda—undefined behavior yang tidak selalu melibatkan memori. Berikut dua contoh klasik.

c
/* File: signed_overflow.c — BUG: signed integer overflow */
#include <stdio.h>
#include <limits.h>

int main(void) {
    int a = INT_MAX;
    int b = 1;
    int c = a + b;   /* BUG: signed overflow (UB) */
    printf("INT_MAX + 1 = %d\n", c);
    return 0;
}

Build dengan UBSan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o signed_overflow signed_overflow.c && ./signed_overflow

Output yang Dihasilkan:

text
signed_overflow.c:8:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
INT_MAX + 1 = -2147483648

UBSan mencatat runtime error dengan baris kode dan ekspresi yang melanggar. Program tetap berjalan (default UBSan tidak abort), tetapi pesan error memperingatkan kita. Untuk null pointer dereference:

c
/* File: null_deref.c — BUG: dereference pointer NULL */
#include <stdio.h>
#include <stddef.h>

int main(void) {
    int *p = NULL;
    *p = 10;   /* BUG: null dereference */
    return 0;
}

Build dengan UBSan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o null_deref null_deref.c && ./null_deref

Output yang Dihasilkan:

text
null_deref.c:7:8: runtime error: store to null pointer of type 'int'

Integrasi Sanitizer ke Test Pipeline #

Untuk memaksimalkan manfaat sanitizer, kita membangun tiga varian binary di CI:

  1. Build normal (-O2 -Wall -Wextra): Performance mirip produksi, untuk memverifikasi fungsionalitas.
  2. Build ASan + UBSan (-O1 -fsanitize=address,undefined): Menjalankan test suite—setiap bug memori atau UB langsung terdeteksi.
  3. Build coverage (-O0 -fprofile-arcs -ftest-coverage): Untuk mengukur cakupan test.

Flag -fno-omit-frame-pointer sering ditambahkan agar backtrace ASan/UBSan menampilkan nama fungsi dengan lengkap, bukan hanya alamat heksadesimal.

Mari kita telaah ringkasan flag yang umum dipakai:

  1. -fsanitize=address: Mengaktifkan ASan—mendeteksi bug memori heap dan stack. Overhead performa ~2x, overhead memori ~3x.
  2. -fsanitize=undefined: Mengaktifkan UBSan—mendeteksi UB aritmatika dan pointer. Overhead performa kecil (<10%).
  3. -fsanitize=leak: Mengaktifkan LSan— mendeteksi memory leak. Biasanya sudah aktif bila ASan dipakai.
  4. -fno-omit-frame-pointer: Mempertahankan frame pointer agar backtrace sanitizer menampilkan nama fungsi. Tanpa flag ini, backtrace hanya berisi alamat memori yang sulit didecode.
  5. -g: Menyertakan debug info sehingga pesan error sanitizer mencantumkan baris kode dan nama file.