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:
/* 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:
=== 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:
- Baris 7-12 (
safe_add): Pemeriksaan kunci adalahif (b > 0 && a > INT_MAX - b). Logika: bilabpositif, hasila + bakan melebihiINT_MAXbilaalebih besar dariINT_MAX - b. Pemeriksaan simetris untukb < 0menangani arah negatif. Penting:INT_MAX - bdi sini sendiri adalah operasi aman karenab > 0menjaminINT_MAX - btidak negatif. - Baris 15-30 (
safe_mul): Empat kasus berdasarkan tandaadanb. Kasusa == 0 || b == 0ditangani awal agar hindari pembagian dengan nol padaINT_MAX / b. Pemeriksaana > INT_MAX / bmembandingkan dengan hasil pembagian—ini bekerja karena pembagian integer bulat ke bawah. - Baris 36-43 (kasus uji): Array berisi enam pasangan nilai, termasuk kasus batas
INT_MAX + 1,INT_MAX + 2,INT_MIN - 1, danINT_MIN * -1(yang seharusnya positif2147483648tetapi tidak muat diint). - 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.
- Baris 58-67 (loop mul): Kasus
INT_MIN * -1ditolak karena hasil yang benar (2147483648) tidak muat diint. Kasus50000 * 50000(2.5 miliar) juga ditolak. Tanpa pemeriksaan ini, hasil wrap-around menjadi-2.5 miliaryang 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:
/* 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:
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o overflow_demo overflow_demo.c
./overflow_demoOutput yang Dihasilkan:
overflow_demo.c:8:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
INT_MAX + 1 = -2147483648UBSan 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
intberisiko overflow, gunakanlong longuntuk perhitungan antara. Perhatikan bahwalong longsendiri 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 mengembalikantruebila operasi overflow. Built-in ini portabel antar GCC/Clang dan lebih ringkas dari pemeriksaan manual.