Chapter sebelumnya menutup gerbang input; chapter ini merapikan apa yang terjadi setelah data diterima. Keamanan memori bukan hanya soal tidak crash dan tidak bocor: program yang menangani rahasia — token, password, kunci — harus memastikan rahasia itu tidak menginap lebih lama dari yang diperlukan, dan program jangka panjang harus tetap bisa bekerja saat alokasi besar gagal. Kita bahas tiga kebiasaan yang saling melengkapi: membersihkan data sensitif secara eksplisit, mencegah memory leak dengan kepemilikan otomatis, dan menangani std::bad_alloc dengan sopan.
Rahasia yang Terlalu Lama Menginap di Memori #
Aturan dasarnya satu kalimat: data sensitif boleh ada di memori selama diperlukan, dan harus ditimpa begitu tugasnya selesai. Sisa byte yang dibiarkan adalah jendela bagi banyak pihak: berkas core dump saat program crash, isi swap ketika halaman memori dipindahkan ke disk, proses lain yang membaca memori bebas, atau log debug yang menampilkan buffer mentah. Semakin lama rahasia menginap, semakin lebar jendelanya.
Hati-hati juga dengan salinan. Setiap penyalinan std::string, setiap pesan log yang menyertakan token, setiap objek sementara menambah buffer lain yang harus dilacak dan dibersihkan. Kebiasaan yang menjaga: kirim rahasia lewat parameter tanpa salinan sebisa mungkin, jangan pernah mencatatnya ke log, dan tetapkan satu pemilik yang bertanggung jawab membersihkan — bukan mengandalkan disiplin seluruh tim.
clear() Tidak Cukup: Menimpa Sebelum Melepas #
Jebakan paling licin justru terlihat benar: rahasia.clear() hanya menyetel panjang logis menjadi nol — byte lama tetap tergeletak di buffer. Solusi keduanya, memset(buf, 0, n), punya penentang yang tak terlihat: compiler yang mengoptimalkan. Penulisan ke memori yang tidak dibaca lagi sebelum dilepas tidak memengaruhi hasil program, sehingga standar mengizinkan compiler menghapusnya (dead store elimination). memset yang menghilang tanpa suara adalah perilaku yang sah — dan itulah kenapa pembersihan rahasia butuh alat khusus.
Program berikut memakai satu token demo, membersihkannya lewat fungsi bersihkan, lalu menantang sistem dengan permintaan alokasi 4 TB yang pasti gagal. Keduanya teruji dan deterministik:
// rahasia-aman.cpp — rahasia dibersihkan eksplisit; kegagalan alokasi ditangani sopan.
#include <iostream>
#include <new>
#include <string>
#include <vector>
// Menulis nol lewat pointer volatile: compiler dilarang menghapus penulisan ini,
// sehingga sisa rahasia benar-benar tertimpa nol sebelum string dilepas.
void bersihkan(std::string& rahasia) {
volatile char* p = rahasia.data();
for (std::size_t i = 0; i < rahasia.size(); ++i) {
p[i] = 0;
}
rahasia.clear();
}
int main() {
std::string token = "demo-token-9f8e7d6c";
std::cout << "token terpakai : " << token.size() << " karakter\n";
bersihkan(token);
std::cout << "setelah dibersihkan : " << token.size() << " karakter, kosong? "
<< std::boolalpha << token.empty() << "\n";
try {
std::vector<int> blok(1'000'000'000'000ull); // 4 TB: jelas tidak muat di mesin ini
std::cout << "alokasi 4 TB berhasil?!\n"; // tidak akan tercapai di lingkungan course
} catch (const std::bad_alloc& e) {
std::cout << "bad_alloc tertangkap : " << e.what() << ", program tetap merespons\n";
}
return 0;
}Output yang Dihasilkan:
token terpakai : 19 karakter
setelah dibersihkan : 0 karakter, kosong? true
bad_alloc tertangkap : std::bad_alloc, program tetap meresponsBedah baris-per-baris:
- Baris 10–16 (
bersihkan) — inti kebiasaan ini: penulisan lewatvolatile char*dilarang dihapus oleh compiler, sehingga byte rahasia benar-benar tertimpa nol;clear()baru dipanggil setelahnya. - Baris 19–20 (pakai seminimal mungkin) — hanya panjang token yang dicetak, isinya tidak pernah tampil di output; kebiasaan yang sama harus berlaku untuk log produksi.
- Baris 22–24 (verifikasi) — setelah pembersihan,
size()bernilai nol danempty()mengembalikantrue; yang kita uji adalah perilaku yang bisa diamati, bukan niat baik. - Baris 26–31 (
bad_alloc) — permintaan satu triliunint(sekitar 4 TB) gagal,std::vectormelaporkannya lewat exceptionstd::bad_alloc, dan program menangkapnya lalu tetap merespons.
explicit_bzero dan memset_s: Sekilas Portabilitas #
Dua nama baku untuk pekerjaan yang sama: explicit_bzero (glibc dan BSD) serta memset_s (C11 Annex K — sifatnya opsional, tidak dijamin ada di semua implementasi). Keduanya memberi janji yang tidak diberikan memset: penulisan nol tidak boleh dioptimalkan. Persoalannya portabilitas: ketersediaannya bergantung platform dan header, dan memanggilnya dari C++ butuh pemeriksaan macro. Pola yang dipakai program di atas — penulisan lewat pointer volatile — memberi jaminan serupa tanpa dependensi apa pun; bila proyek Anda menargetkan platform yang menyediakan fungsi baku, bungkus pilihan platform di satu fungsi bantu:
void bersihkanRahasia(void* alamat, std::size_t n) {
// Pilih satu sesuai platform:
// explicit_bzero(alamat, n); // glibc / BSD
// memset_s(alamat, n, 0, n); // C11 Annex K, bila tersedia
volatile unsigned char* p = static_cast<volatile unsigned char*>(alamat);
while (n--) { *p++ = 0; } // fallback portabel
}Kebocoran Memori dan bad_alloc yang Terkendali #
Memory leak pada program baris perintah yang berjalan sebentar hampir tak terasa; pada server yang hidup berminggu-minggu, setiap request yang bocor beberapa kilobyte akhirnya menghabiskan RAM seluruh mesin — pola serangan yang dikenal sebagai resource exhaustion DoS. Mitigasinya bukan disiplin delete yang sempurna, melainkan kepemilikan otomatis: std::unique_ptr dan std::make_unique melepas memori lewat destructor di semua jalur eksekusi, termasuk jalur exception. Chapter use-after-free sudah menunjukkan polanya; keamanan menambah alasan yang lebih keras: kebocoran bukan sekadar bug performa, ia permukaan serangan yang tumbuh diam-diam.
Saat alokasi gagal, new standar melempar std::bad_alloc alih-alih mengembalikan nullptr — program yang tidak menangkapnya berhenti lewat std::terminate. Menangkap exception di batas operasi (satu request, satu batch) memberi kesempatan menjawab dengan rapi: tolak permintaan itu, lepaskan cache, atau turunkan beban. Ada juga varian new (std::nothrow) yang mengembalikan nullptr; pilih satu konvensi untuk seluruh proyek dan konsisten.
Memori yang bersih dan alokasi yang terkendali menutup sisi internal program. Sisi eksternal berikutnya juga bersentuhan dengan sumber daya: berkas yang dibuka berdasarkan nama yang dikirim pengguna — dan di sanalah celah klasik bernama path traversal menunggu.
Referensi: cppreference — std::bad_alloc , cppreference — std::unique_ptr .