Operasi aritmetika pada int, long, dan tipe signed lainnya dapat menghasilkan nilai yang melampaui batas tipe—keadaan yang disebut signed integer overflow. Standar C menetapkan perilaku ini sebagai undefined behavior, artinya compiler bebas mengasumsikan overflow tidak pernah terjadi dan mengoptimalkan kode dengan cara yang mengejutkan. Eksploitasi klasik memanfaatkan pemeriksaan if (a + b < a) yang dihapus compiler karena dianggap tidak mungkin terjadi. Pada chapter ini kita menulis dua fungsi pembungkus—safe_add dan safe_mul—yang memeriksa kemungkinan overflow sebelum operasi dijalankan.

Mengapa Overflow Adalah Undefined Behavior #

Standard C (ISO/IEC 9899:2011 bagian 6.5) menyatakan bahwa operasi pada nilai-nilai signed yang menghasilkan hasil di luar rentang tipe adalah undefined behavior. Alasan historis: berbagai arsitektur CPU berperilaku berbeda—komplementer-dua, satu komplemen, atau trap. Alih-alih memilih satu, standar membiarkan compiler bebas. Konsekuensinya, compiler modern seperti GCC dan Clang memakai asumsi “tidak ada overflow” untuk mengoptimalkan: loop for (int i = 0; i <= N; i++) tidak akan diakhiri bila N = INT_MAX karena compiler mengasumsikan i tidak akan overflow menjadi negatif.

Pola pemeriksaan yang benar adalah membandingkan operand sebelum operasi:

c
/* integer_overflow_prevention.c */
/* Cek sebelum operasi aritmetika untuk mencegah signed overflow (UB). */
#include <stdio.h>
#include <limits.h>

/* Penjumlahan aman: kembalikan 1 bila sukses, 0 bila overflow. */
static int safe_add(int a, int b, int *result) {
    if ((b > 0 && a > INT_MAX - b) ||
        (b < 0 && a < INT_MIN - b)) {
        return 0; /* overflow terdeteksi */
    }
    *result = a + b;
    return 1;
}

/* Perkalian aman dengan cek terlebih dahulu. */
static int safe_mul(int a, int b, int *result) {
    if (a == 0 || b == 0) { *result = 0; return 1; }
    if (a > 0) {
        if (b > 0) {
            if (a > INT_MAX / b) return 0;
        } else {
            if (b < INT_MIN / a) return 0;
        }
    } else {
        if (b > 0) {
            if (a < INT_MIN / b) return 0;
        } else {
            if (b < INT_MAX / a) return 0;
        }
    }
    *result = a * b;
    return 1;
}

int main(void) {
    struct { int a; int b; } cases[] = {
        { 10, 20 },
        { INT_MAX, 1 },
        { INT_MAX, 2 },
        { INT_MIN, -1 },
        { 1000, 1000 },
        { 50000, 50000 },
    };
    const size_t n = sizeof(cases) / sizeof(cases[0]);

    printf("=== safe_add ===\n");
    for (size_t i = 0; i < n; i++) {
        int r;
        if (safe_add(cases[i].a, cases[i].b, &r)) {
            printf("OK    : %d + %d = %d\n", cases[i].a, cases[i].b, r);
        } else {
            printf("REJECT: %d + %d = (overflow)\n", cases[i].a, cases[i].b);
        }
    }

    printf("\n=== safe_mul ===\n");
    for (size_t i = 0; i < n; i++) {
        int r;
        if (safe_mul(cases[i].a, cases[i].b, &r)) {
            printf("OK    : %d * %d = %d\n", cases[i].a, cases[i].b, r);
        } else {
            printf("REJECT: %d * %d = (overflow)\n", cases[i].a, cases[i].b);
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
=== safe_add ===
OK    : 10 + 20 = 30
REJECT: 2147483647 + 1 = (overflow)
REJECT: 2147483647 + 2 = (overflow)
REJECT: -2147483648 + -1 = (overflow)
OK    : 1000 + 1000 = 2000
OK    : 50000 + 50000 = 100000

=== safe_mul ===
OK    : 10 * 20 = 200
OK    : 2147483647 * 1 = 2147483647
REJECT: 2147483647 * 2 = (overflow)
REJECT: -2147483648 * -1 = (overflow)
OK    : 1000 * 1000 = 1000000
REJECT: 50000 * 50000 = (overflow)

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-12 (safe_add): Pemeriksaan kunci adalah if (b > 0 && a > INT_MAX - b). Logika: bila b positif, hasil a + b akan melebihi INT_MAX bila a lebih besar dari INT_MAX - b. Pemeriksaan simetris untuk b < 0 menangani arah negatif. Penting: INT_MAX - b di sini sendiri adalah operasi aman karena b > 0 menjamin INT_MAX - b tidak negatif.
  2. Baris 15-30 (safe_mul): Empat kasus berdasarkan tanda a dan b. Kasus a == 0 || b == 0 ditangani awal agar hindari pembagian dengan nol pada INT_MAX / b. Pemeriksaan a > INT_MAX / b membandingkan dengan hasil pembagian—ini bekerja karena pembagian integer bulat ke bawah.
  3. Baris 36-43 (kasus uji): Array berisi enam pasangan nilai, termasuk kasus batas INT_MAX + 1, INT_MAX + 2, INT_MIN - 1, dan INT_MIN * -1 (yang seharusnya positif 2147483648 tetapi tidak muat di int).
  4. Baris 46-55 (loop add): Lima dari enam kasus ditangani: satu sukses dengan rentang normal, dua overflow positif, satu overflow negatif, dan dua kasus normal lainnya. Penolakan terjadi sebelum operasi dijalankan, sehingga tidak ada undefined behavior.
  5. Baris 58-67 (loop mul): Kasus INT_MIN * -1 ditolak karena hasil yang benar (2147483648) tidak muat di int. Kasus 50000 * 50000 (2.5 miliar) juga ditolak. Tanpa pemeriksaan ini, hasil wrap-around menjadi -2.5 miliar yang dapat menyebabkan bug logika bisnis (misalnya diskon negatif, alokasi buffer kecil).

Verifikasi dengan UBSan #

Mari kita verifikasi dengan UBSan bahwa operasi INT_MAX + 1 benar-benar tertangkap pada saat runtime. Simpan potongan pendek berikut:

c
/* overflow_demo.c — verifikasi signed overflow ditangkap UBSan. */
#include <stdio.h>
#include <limits.h>

int main(void) {
    int a = INT_MAX;
    int b = 1;
    int c = a + b;            /* UB! */
    printf("INT_MAX + 1 = %d\n", c);
    return 0;
}

Kompilasi dengan -fsanitize=undefined:

bash
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o overflow_demo overflow_demo.c
./overflow_demo

Output yang Dihasilkan:

text
overflow_demo.c:8:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
INT_MAX + 1 = -2147483648

UBSan mendeteksi pelanggaran dan mencatat baris tepatnya. Nilai -2147483648 yang muncul adalah perilaku implementasi, bukan jaminan standar—kita tidak boleh mengandalkannya.

Strategi Lain: Tipe Lebar dan Compiler Built-ins #

Selain pemeriksaan manual, dua pendekatan lain sering dipakai:

  • Promosi ke tipe lebih lebar: Bila int berisiko overflow, gunakan long long untuk perhitungan antara. Perhatikan bahwa long long sendiri dapat overflow pada nilai besar—pendekatan ini tidak menggantikan pemeriksaan bila input pengguna tidak terbatas.
  • Compiler built-ins: GCC dan Clang menyediakan __builtin_add_overflow(a, b, &result) yang mengembalikan true bila operasi overflow. Built-in ini portabel antar GCC/Clang dan lebih ringkas dari pemeriksaan manual.