Undefined Behavior (UB) adalah situasi di mana standar C tidak memberi batasan apa pun pada perilaku program. Compiler bebas melakukan apa saja: menghasilkan hasil acak, crash, menghapus kode yang “tidak mungkin” terjadi, atau bahkan memformat hard disk secara teoretis. Berbeda dari implementation-defined (yang didokumentasikan) dan unspecified (yang memilih dari opsi valid), UB tidak memiliki opsi valid—program menjadi tidak terdefinisi sepenuhnya. Pada chapter ini kita akan memeriksa UB paling umum—signed overflow, null dereference, use-after-free—dan memakai UBSan (Undefined Behavior Sanitizer) untuk mendeteksinya saat runtime.

Signed Overflow adalah UB #

Operasi aritmatika pada signed int yang meluap adalah UB. Misalnya INT_MAX + 1 secara matematis tidak muat di int—standar tidak menentukan hasilnya. Compiler bebas memilih apakah akan wrap-around (two’s complement), memunculkan trap, atau mengoptimasi berdasarkan asumsi “overflow tidak terjadi”.

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

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

    /* Unsigned overflow: well-defined (modulo arithmetic) */
    unsigned int u = 0;
    printf("\nu = 0\n");
    u = u - 1;
    printf("u - 1 = %u (OK: unsigned wrap-around)\n", u);

    /* Null pointer dereference: UB (jangan diaktifkan) */
    int *p = NULL;
    (void)p;  /* supaya tidak warning unused */
    /* *p = 42;  -- jangan diaktifkan, akan crash */

    return 0;
}

Output yang Dihasilkan:

text
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648 (UB: signed overflow)

u = 0
u - 1 = 4294967295 (OK: unsigned wrap-around)

Pada output, INT_MAX + 1 = -2147483648—GCC memilih two’s complement wrap-around. Namun ini BUKAN jaminan. Compiler lain dapat memilih trap (crash), atau lebih buruk lagi: compiler dapat berasumsi a + 1 > a selalu true untuk int signed (karena compiler mengasumsikan tidak ada UB), lalu mengoptimalkan loop bound check yang mengandalkan perilaku overflow.

Berbeda dengan unsigned int, overflow pada unsigned adalah well-defined—standar menetapkan modulo arithmetic 2^N. u - 1 ketika u = 0 menghasilkan UINT_MAX (4294967295 pada 32-bit), bukan UB.

Mari kita telaah baris-baris kode di atas:

  1. Baris 7 (int a = INT_MAX;): a = 2147483647, nilai maksimum int 32-bit.
  2. Baris 9 (int b = a + 1;): penambahan INT_MAX + 1 meluap—UB. Pada GCC x86-64, hasil terlihat -2147483648 karena two’s complement, tetapi tidak dijamin.
  3. Baris 14 (unsigned int u = 0;): variabel unsigned. Overflow didefinisikan sebagai modulo arithmetic.
  4. Baris 16 (u = u - 1;): 0 - 1 pada unsigned int 32-bit menghasilkan UINT_MAX = 4294967295. Bukan UB—standar mendefinisikan ini.
  5. Baris 19 (int *p = NULL;): pointer NULL. Dereferensi *p adalah UB—kernel akan mengirim SIGSEGV. Baris ini hanya mendeklarasikan pointer tanpa dereferensi.

Use-After-Free dan Buffer Overflow #

Bug memori paling berbahaya di C adalah use-after-free, double-free, buffer overflow, dan null dereference. Semua adalah UB—program mungkin crash, mungkin juga terus berjalan dengan data rusak yang dapat dieksploitasi untuk serangan keamanan.

c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    /* Use-after-free: UB */
    int *p = malloc(sizeof(int));
    if (!p) return 1;
    *p = 42;
    printf("sebelum free: *p = %d\n", *p);
    free(p);
    /* Akses setelah free adalah UB */
    /* *p = 99; -- jangan diaktifkan */

    /* Buffer overflow: UB */
    int arr[3] = {1, 2, 3};
    printf("\narr = {%d, %d, %d}\n", arr[0], arr[1], arr[2]);
    /* arr[5] = 100; -- jangan diaktifkan */

    /* Null deref: UB */
    int *q = NULL;
    if (q != NULL) {
        *q = 100;
    } else {
        printf("\nq == NULL, tidak dereferensi (aman)\n");
    }

    return 0;
}

Output yang Dihasilkan:

text
sebelum free: *p = 42

arr = {1, 2, 3}

q == NULL, tidak dereferensi (aman)

Bagian penting dari contoh ini adalah kode yang tidak diaktifkan—tiga baris yang dikomentari (*p = 99, arr[5] = 100, *q = 100) adalah UB. Mengaktifkannya akan menyebabkan program berperilaku tidak terduga: crash SIGSEGV, korupsi data, atau diam-diam merusak memori yang baru saja dialokasikan. Branch if (q != NULL) di akhir menunjukkan pola defensif yang benar: selalu periksa NULL sebelum dereferensi.

Mendeteksi UB dengan Sanitizer #

GCC dan Clang menyediakan UBSan (Undefined Behavior Sanitizer) dan ASan (Address Sanitizer) untuk mendeteksi UB saat runtime. Aktifkan dengan flag -fsanitize=undefined,address.

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

Output dengan UBSan untuk contoh signed overflow:

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

UBSan menambahkan instrumentation yang memeriksa setiap operasi aritmatika, shift, dan konversi. Bila operasi meluap, UBSan mencetak pesan diagnostik ke stderr dan (tergantung flag) melanjutkan eksekusi atau menghentikan program. ASan bekerja serupa untuk bug memori: use-after-free, double-free, buffer overflow, leak. Kombinasi keduanya mendeteksi sebagian besar UB di C.

Daftar UB Umum di C #

  • Signed integer overflow: INT_MAX + 1, INT_MIN - 1, -INT_MIN.
  • Shift negatif atau terlalu besar: 1 << -1, 1 << 32 (untuk int 32-bit).
  • Null pointer dereference: *NULL, ((struct Foo *)NULL)->field.
  • Use-after-free: akses pointer setelah free.
  • Double-free: free(p); free(p);.
  • Buffer overflow: akses arr[i] di luar batas.
  • Reading uninitialized variable: int x; printf("%d", x);.
  • Strict aliasing violation: cast int * ke float * lalu dereferensi.
  • Modifying string literal: char *s = "halo"; s[0] = 'H';.
  • Calling function with wrong prototype: cast function pointer ke tipe yang berbeda.
  • Division by zero: int x = 1/0;.
  • Integer conversion ke tidak dapat direpresentasikan: (int)1e100.