Akses di luar batas (out-of-bounds access) adalah pelanggaran memori paling umum di C++: program membaca atau menulis elemen pada indeks yang tidak dimiliki container. Array mentah dan operator[] tidak memeriksa batas apa pun — indeks yang meleset melampaui batas menjadi undefined behavior, dan di sinilah banyak kerentanan C++ bermula. Chapter ini membedah wujud serangannya, menjelaskan mengapa penulisan jauh lebih berbahaya daripada pembacaan, lalu menutup dengan tiga lapis mitigasi yang dipakai bersama-sama.

Dua Wajah: Baca di Luar Batas dan Tulis di Luar Batas #

Out-of-bounds read mengambil nilai dari memori yang bukan milik array — nilai sampah yang bisa berupa sisa data sensitif dari alokasi lain. Out-of-bounds write jauh lebih berbahaya: program menimpa byte milik objek lain di sekitar buffer, misalnya pointer atau ukuran yang tersimpan di sebelahnya. Penyerang yang bisa mengendalikan indeks dan nilai yang ditulis memperoleh alat untuk mengubah jalannya program — pola inilah yang dipakai banyak eksploitasi nyata.

Pemicu klasiknya membosankan tapi terus berulang: perulangan dengan kondisi i <= ukuran alih-alih i < ukuran, indeks hasil perhitungan yang belum diperiksa, atau array yang menyusut di satu tempat sementara indeks lama masih dipakai di tempat lain. Karena operator[] tidak memeriksa apa pun, ketiga kesalahan itu lolos kompilasi tanpa suara.

Melihat Kegagalan Nyata di bawah ASan #

Program bermasalah berikut membaca dan menulis arr[5] padahal array hanya punya tiga elemen. Kita jalankan di sandbox penulis dengan AddressSanitizer untuk melihat apa yang sebenarnya terjadi:

cpp akses-rentan.cpp — baca dan tulis di luar batas (dijalankan di sandbox, tidak dipaketkan)
#include <iostream>

int main() {
    int arr[3] = {1, 2, 3};
    int i = 5;
    std::cout << arr[i] << "\n";   // OOB read: nilai sampah
    arr[i] = 99;                   // OOB write: menimpa milik tetangga
    return 0;
}
text Contoh output AddressSanitizer (disingkat; alamat berbeda di mesin Anda)
==5296==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f4450f00034 ...
READ of size 4 at 0x7f4450f00034 thread T0
    #0 0x563401731331 in main akses-rentan.cpp:6

Address 0x7f4450f00034 is located in stack of thread T0 at offset 52 in frame
  This frame has 1 object(s):
    [32, 44) 'arr' (line 4) <== Memory access at offset 52 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow akses-rentan.cpp:6 in main

Bagian yang paling berguna dari laporan ini adalah kalimat posisi: arr menempati offset 32 sampai 44 pada frame stack, sementara akses terjadi di offset 52 — delapan byte melewati ujung array. Tanpa ASan, pembacaan itu hanya menghasilkan angka sampah dan penulisannya menimpa byte lain diam-diam; keduanya bisa berbulan-bulan tak terdeteksi sampai program runtuh di mesin pengguna dengan tanda tangan error yang berbeda dari penyebabnya.

operator[] vs at() vs span #

Standar menyediakan beberapa cara mengakses container dengan janji yang berbeda. Memilih yang tepat untuk situasi yang tepat adalah separuh mitigasi:

Cara aksesPemeriksaanBila indeks di luar batasCocok untuk
v[i] (operator[])Tidak adaUBloop dengan batas dari size()
v.at(i)Setiap panggilanMelempar std::out_of_rangeindeks dari luar, jalur berisiko
std::span + cek size()Eksplisit oleh kitaTerserah: tolak atau lemparantarmuka fungsi yang menerima banyak jenis container

Biaya at() bukan nol — pemeriksaan terjadi setiap panggilan — tetapi di jalur yang menerima indeks dari luar (input pengguna, hasil parsing, data jaringan), biaya kecil itu dibayar untuk menutup seluruh kelas kerentanan. Di loop internal yang batasnya diambil langsung dari size(), operator[] tetap pilihan yang wajar.

Versi Aman: Tiga Lapis Pemeriksaan #

Program aman berikut memagari akses di tiga lapis sekaligus: batas loop diambil dari container, fungsi antarmuka memeriksa indeks sebelum menyentuh data, dan akses yang berisiko memakai at():

cpp
// akses-aman.cpp — batas dijaga di tiga lapis: loop dari size(), span, dan at().

#include <iostream>
#include <span>
#include <stdexcept>
#include <vector>

void ambil(std::span<const int> data, std::size_t indeks) {
    if (indeks >= data.size()) {
        std::cout << "span  : indeks " << indeks << " ditolak (size = " << data.size() << ")\n";
        return;
    }
    std::cout << "span  : data[" << indeks << "] = " << data[indeks] << "\n";
}

int main() {
    std::vector<int> nilai = {10, 20, 30};

    for (std::size_t i = 0; i < nilai.size(); ++i) {   // batas diambil dari container
        std::cout << "loop  : nilai[" << i << "] = " << nilai[i] << "\n";
    }

    ambil(nilai, 1);   // sah
    ambil(nilai, 7);   // di luar batas: ditolak oleh pemeriksaan kita sendiri

    try {
        int v = nilai.at(7);
        std::cout << "at(7) : nilai = " << v << "\n";
    } catch (const std::out_of_range&) {
        std::cout << "at(7) : tertangkap std::out_of_range\n";
    }
    return 0;
}

Output yang Dihasilkan:

text
loop  : nilai[0] = 10
loop  : nilai[1] = 20
loop  : nilai[2] = 30
span  : data[1] = 20
span  : indeks 7 ditolak (size = 3)
at(7) : tertangkap std::out_of_range

Bedah baris-per-baris:

  1. Baris 8–13 (ambil(std::span<const int>, std::size_t)) — fungsi menerima data lewat std::span, sehingga panjang selalu bepergian bersama pointer; span<const int> menandakan fungsi hanya membaca.
  2. Baris 9–12 (cek indeks >= data.size()) — indeks yang tidak sah ditolak dengan pesan yang jelas, bukan diproses setengah hati. Perhatikan penulisan >= untuk tipe tak bertanda: indeks size_t yang meluap ke bawah jadi angka besar dan tetap tertangkap di sini.
  3. Baris 17–19 (loop i < nilai.size()) — batas diambil dari container itu sendiri, bukan konstanta yang diketik manual; bila nilai bertambah elemen, loop ikut benar tanpa diubah.
  4. Baris 26–31 (nilai.at(7)) — akses yang sengaja berisiko memakai at(), yang melempar std::out_of_range; exception tertangkap dan program berlanjut dengan sopan.

Dengan tiga lapis ini, indeks yang meleset berubah dari kerentanan diam-diam menjadi penolakan yang terlihat. Persoalan memori berikutnya lebih licin lagi: memori yang sah saat akses dilakukan, tetapi sudah dilepas — use-after-free.

Referensi: cppreference — std::vector::at , cppreference — std::span .