Manajemen memori manual memberi programmer C kendali penuh atas kapan memori dialokasikan dan dilepas. Kebebasan ini datang dengan tanggung jawab: bila free() dipanggil dua kali pada pointer yang sama, atau bila pointer yang sudah di-free() dipakai lagi, program berperilaku tak terduga—kadang crash, kadang bocor informasi, kadang menjadi pintu masuk eksploitasi RCE. CWE-416 (Use After Free) dan CWE-415 (Double Free) mencatat ribuan kasus setiap tahun, terutama pada browser, kernel, dan daemon jaringan. Pada chapter ini kita membangun pola safe_free() yang menetapkan pointer ke NULL setelah dilepas—satu disiplin sederhana yang sekaligus mencegah kedua kelas bug tersebut.

Tiga Kelas Bug Memori #

  • Use-after-free: Pointer tidak di-set NULL setelah free(). Kode berikutnya masih mengakses pointer tersebut, membaca atau menulis memori yang sudah dikembalikan ke allocator. Pada allocator glibc, memori bebas sering berada di free list; bila pengguna lain mengalokasikan ukuran serupa, data pengguna lama dan baru akan menempati alamat yang sama.
  • Double-free: free() dipanggil dua kali pada pointer yang sama. Pada allocator glibc, ini merusak metadata free list dan dapat dimanfaatkan untuk arbitrary write.
  • Memory leak: malloc() tanpa free() yang sesuai. Tidak langsung berbahaya, tetapi pada proses berjalan lama menyebabkan pertumbuhan memori tanpa batas.
c
/* memory_safety.c */
/* Pola pencegahan use-after-free dan double-free. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Wrapper free yang menetapkan pointer ke NULL melalui indirection. */
static void safe_free(void **pp) {
    if (pp == NULL || *pp == NULL) return;
    free(*pp);
    *pp = NULL; /* Cegah use-after-free dan double-free. */
}

/* Alokasi dan jaga pointer. */
static char *make_buffer(const char *msg) {
    char *p = malloc(strlen(msg) + 1);
    if (p == NULL) return NULL;
    strcpy(p, msg);
    return p;
}

int main(void) {
    printf("--- Skenario 1: free yang aman dengan NULL setelahnya ---\n");
    char *buf = make_buffer("data sensitif");
    printf("Sebelum free : buf='%s' (alamat %p)\n", buf, (void*)buf);
    safe_free((void**)&buf);
    printf("Setelah free : buf=%p (NULL? %s)\n", (void*)buf,
           buf == NULL ? "YA" : "TIDAK");

    printf("\n--- Skenario 2: double-free dicegah ---\n");
    char *buf2 = make_buffer("halo");
    printf("Sebelum free : buf2='%s'\n", buf2);
    safe_free((void**)&buf2);
    /* Panggilan kedua aman karena safe_free memeriksa NULL. */
    safe_free((void**)&buf2);
    printf("Setelah free kedua: buf2=%p (NULL? %s)\n",
           (void*)buf2, buf2 == NULL ? "YA" : "TIDAK");

    printf("\n--- Skenario 3: alokasi yang gagal ditangani ---\n");
    /* Simulasikan alokasi sangat besar yang akan gagal. */
    size_t huge = (size_t)-1;
    char *big = malloc(huge);
    if (big == NULL) {
        printf("malloc gagal untuk size=%zu: pointer tetap NULL (aman)\n", huge);
    } else {
        /* Tidak akan tercapai pada umumnya. */
        free(big);
    }
    return 0;
}

Output yang Dihasilkan:

text
--- Skenario 1: free yang aman dengan NULL setelahnya ---
Sebelum free : buf='data sensitif' (alamat 0x55786b1ea2b0)
Setelah free : buf=(nil) (NULL? YA)

--- Skenario 2: double-free dicegah ---
Sebelum free : buf2='halo'
Setelah free kedua: buf2=(nil) (NULL? YA)

--- Skenario 3: alokasi yang gagal ditangani ---
malloc gagal untuk size=18446744073709551615: pointer tetap NULL (aman)

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-12 (safe_free): Fungsi menerima void **pp—pointer ke pointer. Setelah free(*pp), kita menulis *pp = NULL. Penulisan melalui indirection ini mengubah variabel pointer asli di pemanggil, tidak hanya salinan lokal. Pemeriksaan *pp == NULL di awal membuat double-free menjadi no-op yang aman.
  2. Baris 15-19 (make_buffer): Fungsi alokasi yang mengembalikan NULL bila malloc gagal. Pemanggil wajib memeriksa nilai balik sebelum memakai pointer—bila tidak, null pointer dereference akan terjadi. Pemeriksaan if (p == NULL) return NULL; adalah bentuk dasar fail secure.
  3. Baris 22-28 (Skenario 1): Demonstrasi normal. Setelah safe_free, buf bernilai NULL. Bila kode selanjutnya tanpa sengaja menulis buf[0] = 'x';, program akan segfault bersih (NULL dereference) alih-alih menulis ke memori yang sudah di-free()—perilaku yang jauh lebih mudah didiagnosis dan tidak dapat dieksploitasi untuk arbitrary write.
  4. Baris 30-37 (Skenario 2): Panggilan safe_free kedua menemukan *pp == NULL dan langsung kembali tanpa memanggil free(). Tanpa wrapper ini, free(buf2) kedua akan merusak free list allocator—sebuah kondisi yang sering menjadi dasar eksploitasi heap.
  5. Baris 39-48 (Skenario 3): Alokasi (size_t)-1 (yaitu 18446744073709551615 pada sistem 64-bit) pasti gagal. Pemeriksaan if (big == NULL) mencegah program menulis ke pointer yang tidak terinisialisasi.

Verifikasi dengan AddressSanitizer #

Untuk membuktikan bahaya use-after-free, mari kita kompilasi program sederhana dengan dan tanpa AddressSanitizer:

c
/* uaf_demo.c — use-after-free yang ditangkap AddressSanitizer. */
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    char *p = malloc(16);
    p[0] = 'X';
    free(p);
    /* Baris berikut adalah use-after-free. */
    printf("p[0] = %c\n", p[0]);
    return 0;
}

Tanpa sanitizer, program mungkin mencetak p[0] = X (data lama belum ditimpa) atau karakter acak—perilaku tak terduga. Mari kompilasi dengan ASan:

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

Output yang Dihasilkan:

text
=================================================================
==12761==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0x55edbc5a424a bp 0x7fffcf831180 sp 0x7fffcf831178
READ of size 1 at 0x502000000010 thread T0
    #0 0x55edbc5a4249 in main (.../uaf_demo.c:10)
    #1 0x7f8f3780aca7 in __libc_start_main (...libc.so.6)

0x502000000010 is located 0 bytes inside of 16-byte region [0x502000000010,0x502000000020)
freed by thread T0 here:
    #0 0x7f8f37ac88f8 in free (.../asan_malloc_linux.cpp:52)
    #1 0x55edbc5a4215 in main (.../uaf_demo.c:8)

previously allocated by thread T0 here:
    #0 0x7f8f37ac9c57 in malloc (.../asan_malloc_linux.cpp:69)
    #1 0x55edbc5a41ca in main (.../uaf_demo.c:6)

SUMMARY: AddressSanitizer: heap-use-after-free ... in main

ASan melaporkan baris tepat di mana memori di-free() (baris 8), baris di mana malloc dilakukan (baris 6), dan baris di mana akses ilegal terjadi (baris 10). Tanpa ASan, bug ini sering tidak terdeteksi sampai eksploitasi berhasil di lapangan.

Pola Tambahan: Ownership yang Eksplisit #

Pada kode yang lebih besar, bug memori sering muncul karena tidak jelas siapa “pemilik” sebuah alokasi. Konvensi yang membantu:

  • Fungsi yang mengalokasikan mengembalikan ownership ke pemanggil. Pemanggil bertanggung jawab memanggil free().
  • Fungsi yang menerima pointer non-owning tidak boleh memanggil free() pada pointer tersebut. Dokumentasikan dengan komentar (non-owning) atau (borrowed).
  • Container menguasai elemen di dalamnya. Saat container dihancurkan, elemen dilepaskan juga.