Bug memori pada C++ (out-of-bounds, use-after-free, signed overflow) sering tidak terdeteksi pada saat compile. Program berjalan “benar” pada uji sample tetapi crash atau memberi jawaban salah pada input tertentu. Sanitizer menyelesaikan masalah ini dengan menginstrumentasi kode pada saat compile untuk mendeteksi pelanggaran memori pada saat runtime. Pada Chapter ini kita akan membahas empat sanitizer utama — AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer, dan MemorySanitizer — beserta flag compile yang benar dan contoh output realistis. Tujuan akhirnya: setiap kode DSA yang akan dikirim ke judge produksi sebaiknya diuji minimal dengan ASan + UBSan sebelum submit.

Empat Sanitizer Utama #

SanitizerSingkatanFlag CompileMendeteksi
AddressSanitizerASan-fsanitize=addressOut-of-bounds, use-after-free, double-free, memory leak
UndefinedBehaviorSanitizerUBSan-fsanitize=undefinedSigned overflow, null deref, shift berlebih, misaligned access
ThreadSanitizerTSan-fsanitize=threadData race pada multi-threading
MemorySanitizerMSan-fsanitize=memoryRead uninitialized memory

Mekanisme: Instrumentasi Compile-Time #

Saat flag -fsanitize=address dipakai, compiler menyisipkan kode tambahan pada setiap akses memori: pointer diperiksa terhadap shadow memory — peta bitmap yang melacak byte mana yang valid. Bila akses keluar dari region yang valid, program dihentikan dengan pesan error mendetail (lokasi file:baris, alamat, jenis pelanggaran).

UBSan bekerja dengan cara berbeda: compiler menyisipkan cek sebelum operasi yang berpotensi UB (penjumlahan signed, shift, dereference). Bila UB terdeteksi, UBSan mencatat pesan ke stderr tetapi program dapat melanjutkan (default) atau dihentikan (dengan halt_on_error=1).

Overhead runtime sanitizer cukup besar: ASan ~2x slowdown + ~3x memori, TSan ~5-15x slowdown, UBSan ~5-10%. Karena itu sanitizer dipakai untuk build debug/test, bukan untuk build produksi.

Kode: Versi Aman (Tanpa Sanitizer Output) #

cpp
#include <iostream>
#include <vector>
#include <climits>

// 1) Out-of-bounds read — versi aman: cek bounds secara manual.
int safe_at(const std::vector<int>& v, int i) {
    if (i < 0 || i >= (int)v.size()) return -1;
    return v[i];
}

// 2) Signed integer overflow — versi aman: pakai long long.
long long safe_sum(int a, int b) {
    return (long long)a + (long long)b;
}

// 3) Use-after-free — versi aman: salin nilai sebelum delete.
void demo_uaf() {
    int* p = new int(42);
    int value = *p;        // salin nilai
    delete p;
    // Setelah delete, *p adalah UB. Di sini kita hanya pakai value.
    std::cout << "[UAF demo] value sebelum delete: " << value << '\n';
}

// 4) Uninitialized read — versi aman: selalu inisialisasi.
int demo_uninit_read() {
    int x = 0;             // aman: diinisialisasi
    return x + 1;
}

// 5) Null pointer dereference — versi aman: cek nullptr.
int demo_null_deref(int* p) {
    if (p == nullptr) return -1;
    return *p;
}

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

    std::cout << "safe_at(v, 0) = " << safe_at(v, 0) << '\n';
    std::cout << "safe_at(v, 5) = " << safe_at(v, 5) << " (out-of-bounds -> -1)\n";

    std::cout << "safe_sum(INT_MAX, 1) = " << safe_sum(INT_MAX, 1) << '\n';

    demo_uaf();
    std::cout << "demo_uninit_read() = " << demo_uninit_read() << '\n';

    int x = 7;
    std::cout << "demo_null_deref(&x) = " << demo_null_deref(&x) << '\n';
    std::cout << "demo_null_deref(nullptr) = " << demo_null_deref(nullptr) << '\n';

    std::cout << "\n=== Compile flags yang direkomendasikan ===\n";
    std::cout << "ASAN  : g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer\n";
    std::cout << "UBSAN : g++ -std=c++17 -O1 -g -fsanitize=undefined\n";
    std::cout << "TSAN  : g++ -std=c++17 -O1 -g -fsanitize=thread\n";
    std::cout << "MSAN  : g++ -std=c++17 -O1 -g -fsanitize=memory\n";
    return 0;
}

Output yang Dihasilkan (tanpa sanitizer):

text
safe_at(v, 0) = 10
safe_at(v, 5) = -1 (out-of-bounds -> -1)
safe_sum(INT_MAX, 1) = 2147483648
[UAF demo] value sebelum delete: 42
demo_uninit_read() = 1
demo_null_deref(&x) = 7
demo_null_deref(nullptr) = -1

=== Compile flags yang direkomendasikan ===
ASAN  : g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer
UBSAN : g++ -std=c++17 -O1 -g -fsanitize=undefined
TSAN  : g++ -std=c++17 -O1 -g -fsanitize=thread
MSAN  : g++ -std=c++17 -O1 -g -fsanitize=memory

Bedah Kode Baris-per-Baris #

  1. if (i < 0 || i >= (int)v.size()) return -1;: validasi indeks sebelum akses. Tanpa cek, v[5] pada vector berukuran 3 adalah out-of-bounds read — ASan akan melaporkan heap-buffer-overflow.
  2. (long long)a + (long long)b: cast ke long long sebelum penjumlahan untuk menghindari signed overflow di int. Untuk INT_MAX + 1, hasil int overflow menjadi -2147483648 (UB). UBSan akan melaporkan signed integer overflow.
  3. int value = *p; delete p;: salin nilai sebelum delete. Setelah delete, akses *p adalah use-after-free — ASan melaporkan heap-use-after-free. Dengan menyalin dulu, kita tidak pernah mengakses memori yang sudah di-free.
  4. int x = 0;: inisialisasi eksplisit. Tanpa inisialisasi, int x; berisi nilai tak terduga (MSan melaporkan use-of-uninitialized-value).
  5. if (p == nullptr) return -1;: cek nullptr sebelum dereference. Tanpa cek, *nullptr adalah segfault atau UB — UBSan melaporkan null pointer dereference.

Contoh Output ASan (Heap Buffer Overflow) #

Kode berikut mengakses v[5] pada vector {10, 20, 30} (size 3):

cpp
#include <iostream>
#include <vector>
int main() {
    std::vector<int> v = {10, 20, 30};
    std::cout << "v[5] = " << v[5] << '\n';
    return 0;
}

Kompilasi dan jalankan dengan ASan:

bash
g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer -o demo demo.cpp
./demo

Output yang Dihasilkan (ASan aktif, dipotong untuk keterbacaan):

text
=================================================================
==23245==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000024
READ of size 4 at 0x502000000024 thread T0
    #0 0x... in main /home/z/wave10a/sanitizers_bug_demo.cpp:11
    #1 0x... in __libc_start_main ...

0x502000000024 is located 8 bytes after 12-byte region [0x502000000010,0x50200000001c)
allocated by thread T0 here:
    #0 0x... in operator new(unsigned long)
    #1 0x... in std::vector<int>::vector(...)
    #6 0x... in main /home/z/wave10a/sanitizers_bug_demo.cpp:8

SUMMARY: AddressSanitizer: heap-buffer-overflow
  /home/z/wave10a/sanitizers_bug_demo.cpp:11 in main

ASan melaporkan lima hal kunci:

  1. Jenis pelanggaran: heap-buffer-overflow.
  2. Alamat memori yang diakses: 0x502000000024.
  3. Operasi: READ of size 4 (akses membaca 4 byte, sesuai sizeof(int)).
  4. Lokasi kode: file sanitizers_bug_demo.cpp baris 11.
  5. Lokasi alokasi: baris 8 tempat vector dialokasikan. Pesan “8 bytes after 12-byte region” menjelaskan bahwa akses keluar 8 byte setelah akhir region 12-byte (3 elemen × 4 byte).

Contoh Output UBSan (Signed Integer Overflow) #

cpp
#include <iostream>
#include <climits>
int main() {
    int c = INT_MAX + 1;  // signed overflow — UB
    std::cout << "INT_MAX + 1 = " << c << '\n';
    return 0;
}
bash
g++ -std=c++17 -O1 -g -fsanitize=undefined -o demo demo.cpp
./demo

Output yang Dihasilkan (UBSan aktif):

text
sanitizers_ubsan_demo.cpp:5:13: runtime error: signed integer overflow:
  2147483647 + 1 cannot be represented in type 'int'
INT_MAX + 1 = -2147483648

UBSan melaporkan:

  1. File:baris: sanitizers_ubsan_demo.cpp:5.
  2. Kolom: 13 — lokasi ekspresi yang overflow.
  3. Pesan: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'.
  4. Program tetap berjalan dan mencetak hasil (UBSan default: log-only). Untuk menghentikan program saat UB, tambahkan UBSAN_OPTIONS=halt_on_error=1.

Kombinasi ASan + UBSan untuk DSA #

Kombinasi yang paling berguna untuk DSA single-threaded:

bash
g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
    -o solusi solusi.cpp
./solusi < input.txt

Tambahkan -fsanitize=leak (bagian dari ASan) untuk mendeteksi memory leak. Flag -fno-omit-frame-pointer menjaga stack trace tetap akurat.

Ringkasan #

  • Empat sanitizer utama: ASan (memori), UBSan (UB), TSan (thread), MSan (uninitialized).
  • ASan dan UBSan kompatibel — kombinasi ideal untuk DSA: -fsanitize=address,undefined.
  • ASan mendeteksi out-of-bounds, use-after-free, double-free; UBSan mendeteksi signed overflow, null deref, misaligned access.
  • Overhead runtime sanitizer besar (2x–15x) — pakai hanya untuk build debug/test.
  • ASan + UBSan + edge case test + differential test = jaring pengaman komprehensif sebelum submit ke judge.