Use after free (UAF) terjadi ketika kode membaca atau menulis ke blok heap yang sudah dibebaskan dengan free. Berbeda dengan double free yang langsung kentara (program biasanya crash), UAF sering diam-diam: program mungkin membaca data lama yang tampak valid, lalu membuat keputusan berdasarkan data sampah tersebut. Inilah salah satu bug keamanan paling berbahaya di C—banyak CVE tinggi berasal dari UAF di kernel, browser, dan daemon jaringan. Untungnya, AddressSanitizer (ASan) dapat mendeteksi UAF dengan akurat. Pada chapter ini kita akan membuat program yang sengaja melakukan UAF, lalu memverifikasi ASan menangkapnya.

Apa itu Use After Free #

UAF adalah akses (baca atau tulis) ke memori heap setelah free dipanggil pada blok tersebut. Karakteristik UAF:

  • Tipe akses: bisa baca (*p), tulis (*p = ...), atau bahkan free lain (yang juga merupakan double free).
  • Konsekuensi bervariasi: bila blok belum dipakai ulang oleh allocator, isi lama mungkin masih ada sehingga program “berperilaku normal”. Bila blok sudah dipakai ulang atau dikembalikan ke OS, akses bisa menyebabkan segfault atau membaca data dari alokasi lain (data leak).
  • Sulit didiagnosis tanpa alat: bug tidak selalu muncul saat testing—sering muncul hanya di load tertentu atau di sistem operasi tertentu.

Penyebab umum UAF:

  • Dangling pointer — free(p) tanpa p = NULL; lalu *p tanpa sengaja.
  • Cache yang tidak diinvalidasi — cache global menyimpan pointer ke blok yang sudah di-free di tempat lain.
  • Pointer alias — int *a = malloc(...); int *b = a; free(a); *b; — b tidak ikut di-NULL-kan.
  • Callback setelah destroy — event handler dipanggil setelah objek yang dimilikinya dihancurkan.

Demo: Use After Free #

Kita alokasikan satu int, isi nilai 12345, lalu free dan langsung membaca kembali tanpa ASan terlebih dahulu untuk membandingkan perilaku.

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

int main(void) {
    int *p = malloc(sizeof(int));
    if (p == NULL) return 1;
    *p = 12345;
    printf("Sebelum free: *p = %d, alamat = %p\n", *p, (void*)p);

    free(p);
    fflush(stdout);

    /* BUG: use-after-free - mengakses blok yang sudah dibebaskan */
    printf("Setelah free : *p = %d (UB!)\n", *p);
    return 0;
}

Kompilasi dengan ASan dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined \
    -o /tmp/test_uaf /home/z/my-project/work/tests/test_uaf.c
/tmp/test_uaf

Output yang Dihasilkan:

text
Sebelum free: *p = 12345, alamat = 0x502000000010
=================================================================
==24550==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0x55ee589d335f bp 0x7ffdf2aeb300 sp 0x7ffdf2aeb2f8
READ of size 4 at 0x502000000010 thread T0
    #0 0x55ee589d335e in main (/tmp/test_uaf+0x135e)
    #1 0x7f77adf48ca7 in (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7)
    #2 0x7f77adf48d64 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d64)

0x502000000010 is located 0 bytes inside of 4-byte region [0x502000000010,0x502000000014)
freed by thread T0 here:
    #0 0x7f77ae8198f8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x55ee589d32cc in main (/tmp/test_uaf+0x12cc)

previously allocated by thread T0 here:
    #0 0x7f77ae81ac57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x55ee589d31fa in main (/tmp/test_uaf+0x11fa)

SUMMARY: AddressSanitizer: heap-use-after-free (/tmp/test_uaf+0x135e) in main

ASan mendeteksi READ of size 4 pada alamat 0x502000000010—persis lokasi p yang sudah di-free. ASan juga melaporkan “freed by thread T0 here” yang menunjuk ke baris free(p) kita, dan “previously allocated by thread T0 here” yang menunjuk ke baris malloc. Informasi ini sangat akurat—kita tahu persis baris mana yang menjadi masalah.

Tanpa ASan, program mungkin saja mencetak *p = 12345 (data lama masih ada di blok yang belum dipakai ulang), *p = 0 (allocator men-zero-kan blok bebas untuk deteksi), atau segfault (blok sudah dipetakan ulang ke alamat tak terbaca). Ketidakpastian inilah yang membuat UAF sangat sulit didiagnosis tanpa alat bantu.

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (int *p = malloc(sizeof(int));): alokasi 4 byte di heap. ASan menempatkan blok di “shadow memory” region 0x502... agar dapat melacak status setiap byte.
  2. Baris 8 (*p = 12345;): menulis nilai ke blok. Operasi sah—blok masih dimiliki program.
  3. Baris 9 (printf("Sebelum free: *p = %d, alamat = %p\n", *p, (void*)p);): mencetak nilai dan alamat sebelum free. Karena %p memerlukan void*, kita melakukan cast (void*)p.
  4. Baris 11 (free(p);): mengembalikan blok ke allocator. Setelah baris ini, p adalah dangling pointer—masih menyimpan alamat lama, tetapi blok sudah tidak dimiliki program.
  5. Baris 12 (fflush(stdout);): memaksa pembilasan buffer stdout ke terminal. Tanpa baris ini, pesan “Sebelum free” bisa hilang bila ASan membatalkan program sebelum buffer dibilas.
  6. Baris 15 (printf("Setelah free : *p = %d (UB!)\n", *p);): BUG use-after-free. Ekspresi *p melakukan dereference pada p yang sekarang menunjuk ke blok yang sudah di-free. ASan mendeteksi operasi READ ini dan membatalkan program.

Mencegah UAF #

Beberapa pola untuk mencegah UAF:

  1. Set pointer ke NULL setelah free — pola paling sederhana. Dereference pointer NULL akan segera segfault (sebuah crash yang jelas), bukan diam-diam membaca data sampah.
  2. Hindari pointer alias — bila int *b = a; dibuat, pastikan b juga di-set NULL ketika a di-free. Atau gunakan reference counting.
  3. Bersihkan cache — bila ada cache global yang menyimpan pointer ke blok, invalidasi cache sebelum free.
  4. Gunakan arena allocator atau obstack — untuk program yang membutuhkan banyak alokasi sementara dengan lifetime bersama, gunakan arena yang dibebaskan sekaligus, sehingga tidak ada dangling pointer individu.
  5. Gunakan ASan di CI — tes otomatis dengan ASan menangkap UAF yang tidak terlihat dalam testing fungsional.

Pola Aman: Set NULL, Lalu Cek sebelum Dereference #

Pola paling sederhana untuk mencegah UAF: set pointer ke NULL setelah free, lalu selalu cek NULL sebelum dereference. Dereference pointer NULL akan segera segfault—crash yang jelas dan dapat didiagnosis—bukan diam-diam membaca data sampah.

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

int main(void) {
    int *p = malloc(sizeof(int));
    if (p == NULL) return 1;
    *p = 99;
    printf("Sebelum free: *p = %d\n", *p);

    free(p);
    p = NULL;   /* mencegah UAF: pointer tidak lagi menunjuk ke blok lama */

    /* Cek NULL sebelum dereference */
    if (p != NULL) {
        printf("*p = %d\n", *p);
    } else {
        printf("p = NULL, dereference dilewati (tidak UAF)\n");
    }

    return 0;
}

Output yang Dihasilkan:

text
Sebelum free: *p = 99
p = NULL, dereference dilewati (tidak UAF)

Setelah p = NULL, ekspresi *p menjadi undefined behavior bila dipanggil—tetapi pengecekan if (p != NULL) terlebih dahulu memastikan kita tidak pernah mencapai titik itu. Cabang else dijalankan karena p adalah NULL, dan tidak ada akses ke memori yang sudah dibebaskan. Inilah pola minimum yang harus selalu kita pakai setelah free.