Use-after-free terjadi ketika program menyentuh memori melalui pointer yang bloknya sudah dilepas dengan delete. Pointer itu masih berisi alamat yang sama, sehingga dereferensi tampak “berhasil” — tetapi dari sudut pandang standar, objeknya sudah tidak ada dan aksesnya adalah undefined behavior. Yang membuat kelas bug ini terkenal bukan kerumitannya, melainkan sifatnya: kegagalan biasanya muncul jauh dari penyebabnya, di mesin pengguna, dan sering bisa dimanfaatkan penyerang. Chapter ini membedah mekanismenya, pola kode yang memicunya, dan mitigasi yang sudah kita kenal dari Section Manajemen Memori — kini dengan sudut pandang keamanan.

Mengapa Blok yang Sudah Dilepas Tetap Berbahaya #

Setelah delete, alamatnya tidak segera “hangus” — alokator menandai blok sebagai bebas dan berhak membagikannya ke permintaan berikutnya. Objek lain bisa menempati byte yang sama beberapa saat kemudian: string yang baru dibuat, objek yang dihapus dan dibuat ulang, atau struktur data internal allocator sendiri. Membaca melalui pointer lama berarti membaca data milik pihak lain; menulis berarti merusaknya. Bila penyerang mengendalikan data yang mengisi blok tersebut, dia mengendalikan apa yang dibaca program melalui pointer basi itu — pola yang dipakai banyak eksploitasi pada interpreter dan browser.

Yang paling mengecoh, program yang bermasalah biasanya tetap berjalan di mesin developer. Bloknya belum dipakai ulang, isinya masih menyerupai data lama, dan semuanya terlihat normal. Kegagalan baru muncul ketika pola alokasi berubah — build rilis dengan optimizer berbeda, beban berbeda, atau versi library berbeda.

Pola yang Sering Memicunya #

Empat pola ini menjadi sumber mayoritas use-after-free di kode nyata:

  • Menyimpan pointer mentah yang diambil dari objek pemilik (get() pada smart pointer, data() pada container), lalu melepas pemiliknya sementara pointer mentah masih dipakai.
  • Melepas objek yang terdaftar di tempat lain — callback, observer, map penunjuk — tanpa menghapus pendaftarannya; panggilan berikutnya menuju objek yang sudah tiada.
  • Melanjutkan iterasi setelah container mengubah ukuran: erase() pada std::vector menggusur elemen dan membuat iterator/pointer/referensi lama menunjuk ke data yang bergeser.
  • Menerima this dari objek yang sudah dihapus, misalnya lewat callback async yang belum dibatalkan.

Bentuk dasarnya bisa sangat kecil. Program berikut baca-tulis empat baris, tetapi baris terakhirnya adalah use-after-free murni. Kita jalankan di sandbox penulis dengan AddressSanitizer:

cpp pemilik-rentan.cpp — membaca memori setelah delete (dijalankan di sandbox, tidak dipaketkan)
#include <iostream>

int main() {
    int* p = new int(42);
    std::cout << "sebelum: " << *p << "\n";
    delete p;
    std::cout << "sesudah: " << *p << "\n";   // use-after-free
    return 0;
}
text Contoh output AddressSanitizer (disingkat; alamat berbeda di mesin Anda)
==6792==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 ...
READ of size 4 at 0x502000000010 thread T0
    #0 0x55abee119314 in main pemilik-rentan.cpp:7

0x502000000010 is located 0 bytes inside of 4-byte region [0x502000000010,0x502000000014)
freed by thread T0 here:
    #1 0x55abee1192c0 in main pemilik-rentan.cpp:6

previously allocated by thread T0 here:
    #1 0x55abee1191da in main pemilik-rentan.cpp:4

SUMMARY: AddressSanitizer: heap-use-after-free pemilik-rentan.cpp:7 in main

Laporan ini menampilkan tiga garis waktu objek dalam satu tampilan: dialokasi di baris 4, dilepas di baris 6, lalu dibaca di baris 7. Tanpa ASan, keempat baris program itu mencetak 42 dua kali dan keluar dengan sukses di sebagian besar mesin — bukti nyata bahwa “jalan normal” bukan tanda bebas bug.

Versi Aman: Kepemilikan yang Mengatur Dirinya #

Mitigasi intinya satu kalimat: jangan ada delete manual, biarkan pemilik yang melepas. Program berikut menampung tiga int dalam buffer milik std::unique_ptr, melepasnya di satu titik yang jelas, dan memanfaatkan konversi ke bool untuk menjaga setiap akses setelahnya:

cpp
// pemilik-aman.cpp — kepemilikan tunggal otomatis: tidak ada akses setelah pelepasan.

#include <iostream>
#include <memory>

int main() {
    auto data = std::make_unique<int[]>(3);
    data[0] = 10;
    data[1] = 20;
    data[2] = 30;

    for (int i = 0; i < 3; ++i) {
        std::cout << "data[" << i << "] = " << data[i] << "\n";
    }

    data.reset();   // dilepas tepat satu kali, di titik yang jelas
    std::cout << "setelah reset: data kosong? " << std::boolalpha << (data == nullptr) << "\n";

    if (data) {
        std::cout << data[0] << "\n";   // tidak akan pernah dieksekusi
    } else {
        std::cout << "pemilik kosong: tidak ada memori untuk diakses\n";
    }
    return 0;
}

Output yang Dihasilkan:

text
data[0] = 10
data[1] = 20
data[2] = 30
setelah reset: data kosong? true
pemilik kosong: tidak ada memori untuk diakses

Bedah baris-per-baris:

  1. Baris 6 (std::make_unique<int[]>(3)) — alokasi array dibungkus std::unique_ptr<int[]>; pelepasan otomatis terjadi tepat satu kali saat pemilik berakhir, tanpa delete yang ditulis tangan.
  2. Baris 8–14 (isi dan baca data[i]) — semua akses terjadi selagi pemilik masih hidup, sehingga setiap dereferensi sah.
  3. Baris 16 (data.reset()) — pelepasan eksplisit di titik yang terbaca: satu pemilik, satu pelepasan. Setelah baris ini, data bernilai nullptr.
  4. Baris 18 dan 20–25 (cek data == nullptr dan if (data)) — kondisi kosong/penuh diperiksa sebelum akses; cabang if yang menyentuh memori tidak akan pernah dieksekusi ketika pemilik kosong.

nullptr Setelah delete: Bantuan yang Terbatas #

Kebiasaan lama menyarankan p = nullptr; setelah delete p. Ide itu benar sebagian: pointer yang sama tidak bisa lagi didereferensi diam-diam, karena dereferensi nullptr segera runtuh dan mudah ditelusuri di debugger. Tetapi bantuan itu berhenti di pointer yang kita nolkan — salinan lain dari alamat yang sama (parameter, anggota class, elemen container) tetap basi dan tidak tahu apa-apa. Dua pointer ke satu objek memang masalah desain: selama kepemilikan tidak tunggal, tidak ada trik pointer yang menyelamatkan.

Dengan kepemilikan otomatis, seluruh kelas use-after-free menyusut menjadi pilihan desain yang salah — bukan race melawan alokator. Babak berikutnya menyerang bug memori yang paling tua dan paling banyak dieksploitasi: buffer overflow pada data teks.

Referensi: cppreference — std::unique_ptr , cppreference — std::make_unique .