Double free adalah bug ketika kita memanggil free dua kali pada blok yang sama. Konsekuensinya undefined behavior—bisa crash segera, bisa juga merusak struktur internal allocator dan menyebabkan crash acak di kemudian hari, atau lebih buruk lagi: heap corruption yang bisa dieksploitasi penyerang untuk eksekusi kode acak. Karena konsekuensinya begitu serius, GCC dan Clang menyertakan AddressSanitizer yang bisa mendeteksi double-free secara akurat pada saat testing. Pada chapter ini kita akan membuat program yang sengaja melakukan double-free, lalu memverifikasi bahwa ASan menangkapnya sebelum crash terjadi.

Apa itu Double Free #

Double free adalah pemanggilan free(p) lebih dari sekali pada p yang menunjuk blok yang sama, tanpa malloc baru di antara dua free tersebut. Konsekuensinya:

  • Perilaku tidak terdefinisi — standar C menyebutnya undefined behavior (UB).
  • Allocator corruption — allocator menandai blok sebagai “bebas” dua kali, yang dapat menyebabkan blok yang sama dikembalikan untuk dua malloc berikutnya—dua pointer berbeda menunjuk ke blok yang sama, dan saling menimpa data.
  • Eksploitasi keamanan — heap corruption akibat double-free adalah vektor klasik serangan heap exploitation; penyerang dapat menyusun payload yang menyalahgunakan metadata allocator untuk mengeksekusi kode acak.

Mekanisme kerja: saat free pertama dipanggil, allocator menandai blok sebagai “bebas” di free list. Saat free kedua dipanggil pada blok yang sama, allocator menemukan blok sudah “bebas”—perilaku allocator berbeda-beda. Glibc memiliki double-free detection untuk fastbin yang dapat memicu abort dengan pesan “double free detected”. Namun, deteksi ini tidak lengkap dan dapat dilewati dengan urutan alokasi tertentu. ASan jauh lebih andal untuk menangkap double-free.

Demo: Double Free #

Kita tulis program sederhana yang mengalokasikan blok, membebaskannya, lalu mencoba membebaskannya lagi. Tanpa ASan, program mungkin crash atau berperilaku aneh; dengan ASan, kita mendapatkan laporan yang jelas.

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

int main(void) {
    int *p = malloc(4 * sizeof(int));
    if (p == NULL) return 1;
    p[0] = 11; p[1] = 22; p[2] = 33; p[3] = 44;
    printf("Sebelum free: p[0]=%d, alamat=%p\n", p[0], (void*)p);

    free(p);
    printf("Free pertama berhasil\n");
    fflush(stdout);

    /* BUG: double free - memanggil free lagi pada blok yang sama */
    free(p);
    printf("Baris ini biasanya tidak tercapai\n");
    return 0;
}

Kompilasi dengan ASan dan jalankan:

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

Output yang Dihasilkan:

text
Sebelum free: p[0]=11, alamat=0x502000000010
Free pertama berhasil
=================================================================
==24449==ERROR: AddressSanitizer: attempting double-free on 0x502000000010 in thread T0:
    #0 0x7f52ac5078f8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x5615ec9b24d2 in main (/tmp/test_double_free+0x24d2)
    #2 0x7f52abc36ca7 in (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7)
    #3 0x7f52abc36d64 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d64)

0x502000000010 is located 0 bytes inside of 16-byte region [0x502000000010,0x502000000020)
freed by thread T0 here:
    #0 0x7f52ac5078f8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x5615ec9b2484 in main (/tmp/test_double_free+0x2484)

previously allocated by thread T0 here:
    #0 0x7f52ac508c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x5615ec9b220b in main (/tmp/test_double_free+0x220b)

SUMMARY: AddressSanitizer: double-free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52 in free
==24449==ABORTING

Perhatikan bahwa printf("Free pertama berhasil\n") muncul di output, tetapi printf("Baris ini biasanya tidak tercapai\n") tidak muncul—ASan membatalkan program tepat di free(p) kedua. Kita juga menggunakan fflush(stdout) setelah printf pertama karena ASan membatalkan program melalui _exit() yang tidak menyiram buffer stdout.

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (int *p = malloc(4 * sizeof(int));): alokasi 16 byte di heap. Alamat yang dikembalikan ASan (0x502000000010) adalah alamat “shadow memory” ASan, bukan alamat heap asli—ASan menyimpan blok di wilayah khusus untuk melacak alokasi.
  2. Baris 8 (p[0] = 11; ...): menulis nilai ke blok. Operasi ini sah karena blok masih dimiliki program.
  3. Baris 10 (free(p);): free pertama. Blok dikembalikan ke allocator. Setelah baris ini, p secara teknis adalah dangling pointer—alamat masih disimpan, tetapi blok sudah tidak lagi dimiliki program.
  4. Baris 11 (printf("Free pertama berhasil\n");): pesan sukses. Karena printf melakukan line buffering, pesan ini segera muncul di terminal saat newline ditemui—tetapi saat program crash mendadak, buffer yang belum dibilas bisa hilang.
  5. Baris 12 (fflush(stdout);): memaksa pembilasan buffer stdout ke terminal sebelum operasi berbahaya berikutnya. Tanpa baris ini, pesan “Free pertama berhasil” bisa hilang bila program crash sebelum buffer dibilas.
  6. Baris 15 (free(p);): BUG double free. ASan menangkap di titik ini, mencetak stack trace, dan membatalkan program. Tanpa ASan, blok akan ditandai “bebas” dua kali di free list, yang dapat menyebabkan kerusakan allocator.
  7. Baris 16 (printf("Baris ini biasanya tidak tercapai\n");): tidak pernah dieksekusi karena ASan membatalkan program di baris 15.

Penyebab Umum Double Free #

Double free jarang terjadi secara langsung seperti di atas. Penyebab yang lebih umum adalah:

  • Branch error — fungsi keluar lebih awal pada error path, lalu fungsi pemanggil juga memanggil free di cleanup path.
  • Ownership ambigu — dua fungsi mengira dirinya pemilik (lihat chapter Ownership dan Lifetime).
  • Reuse pointer tanpa realloc — free(p); ... free(p); karena p tidak di-set NULL.
  • Cleanup di destructor — destructor objek memanggil free, tetapi destructor dipanggil dua kali (double destruction).

Solusi universal: selalu set pointer ke NULL setelah free. Karena free(NULL) adalah no-op yang aman, setiap free kedua pada pointer yang sudah di-NULL-kan tidak menimbulkan bug.

Pola Aman: Set NULL setelah free #

Karena free(NULL) adalah no-op yang aman, menyetel pointer ke NULL setelah free secara otomatis mencegah double free dan use-after-free dalam satu pola sederhana.

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

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

    free(p);
    p = NULL;   /* pola aman: set ke NULL */

    /* free(p) kedua sekarang aman karena p = NULL */
    free(p);
    printf("free(NULL) tidak crash, program berjalan normal\n");

    /* Cek pointer sebelum dereference */
    if (p == NULL) {
        printf("p adalah NULL, akses aman dilewati\n");
    }

    return 0;
}

Output yang Dihasilkan:

text
Sebelum free: *p = 42
free(NULL) tidak crash, program berjalan normal
p adalah NULL, akses aman dilewati

Panggilan free(p) kedua tidak menyebabkan double free karena p sudah di-set ke NULL sebelumnya. free(NULL) adalah no-op yang dijamin aman oleh standar C—tidak melakukan apa pun. Selain itu, pengecekan if (p == NULL) memastikan kita tidak pernah melakukan dereference pada pointer yang sudah tidak valid. Dengan pola satu baris p = NULL, dua bug (double free dan use-after-free) secara otomatis dicegah.