Sebuah parser mungkin lulus semua unit test dengan input yang dipikirkan, tetapi crash misterius muncul saat pengguna mengirim data yang tidak terduga. Fuzz testing menyelesaikan masalah ini dengan memberi makan input acak—byte, string, atau struktur data—ke program dan mengamati apakah program crash, hang, atau melanggar kontrak. Berbeda dengan property-based testing yang memverifikasi properti output, fuzz testing berfokus pada robustness: program harus tetap aman bahkan untuk input yang tidak masuk akal. Pada chapter ini kita membangun fuzz loop sederhana yang memberi makan 5.000 input acak ke parser KEY=VALUE, lalu mendemonstrasikan bagaimana ASan menemukan buffer overflow yang luput dari implementasi buggy.
Strategi Fuzz Loop #
Fuzz loop minimal terdiri dari empat langkah:
- Generate input acak dengan panjang dan isi yang bervariasi.
- Panggil fungsi yang diuji dengan input tersebut.
- Tangkap sinyal crash (
SIGSEGV,SIGABRT,SIGFPE) dengansetjmp/longjmp. - Catat input yang menyebabkan crash untuk reproduksi.
Untuk fungsi yang tidak crash tetapi menghasilkan output salah, kita memerlukan oracle (pemeriksa tambahan). Fuzz loop berikut berfokus pada deteksi crash.
Parser KEY=VALUE yang Diuji #
Parser membaca buffer dengan format KEY=VALUE, menyalin KEY ke out_key dan VALUE ke out_val. Implementasi berikut sudah aman: memeriksa kapasitas buffer dan mengembalikan -1 bila input tidak valid.
/* File: parser.h */
#ifndef FUZZ_PARSER_H
#define FUZZ_PARSER_H
#include <stddef.h>
/* Parse "KEY=VALUE" dari buffer.
* Kembalikan jumlah byte yang dikonsumsi bila sukses, -1 bila gagal.
* Tidak boleh membaca di luar len. */
int parse_kv(const char *buf, size_t len, char *out_key, size_t key_cap,
char *out_val, size_t val_cap);
#endif/* File: parser.c — implementasi aman */
#include "parser.h"
int parse_kv(const char *buf, size_t len, char *out_key, size_t key_cap,
char *out_val, size_t val_cap) {
if (buf == NULL || out_key == NULL || out_val == NULL) return -1;
if (key_cap == 0 || val_cap == 0) return -1;
size_t i = 0;
size_t k = 0;
while (i < len && buf[i] != '=' && buf[i] != '\0') {
if (k >= key_cap - 1) return -1; /* key terlalu panjang */
out_key[k++] = buf[i++];
}
out_key[k] = '\0';
if (i >= len || buf[i] != '=') return -1; /* tidak ada '=' */
i++; /* lewati '=' */
size_t v = 0;
while (i < len && buf[i] != '\0') {
if (v >= val_cap - 1) return -1; /* value terlalu panjang */
out_val[v++] = buf[i++];
}
out_val[v] = '\0';
if (v == 0) return -1; /* value kosong dianggap invalid */
return (int)i;
}Fuzz Loop dengan Penanganan Sinyal #
Fuzz loop berikut menjalankan 5.000 trial. Setiap trial menghasilkan buffer acak dengan panjang 0 hingga 63 byte, lalu memanggil parse_kv. Bila parse_kv menyebabkan crash (SIGSEGV, SIGABRT, SIGFPE), handler longjmp kembali ke loop dan input yang menyebabkan crash dicetak.
/* File: fuzz.c — fuzz loop dengan penanganan sinyal */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include "parser.h"
static jmp_buf fuzz_buf;
static void sig_handler(int sig) {
(void)sig;
longjmp(fuzz_buf, 1);
}
int main(void) {
signal(SIGSEGV, sig_handler);
signal(SIGABRT, sig_handler);
signal(SIGFPE, sig_handler);
srand(12345); /* seed deterministik */
size_t total_trials = 5000;
size_t max_len = 64;
int crashes = 0;
int valid_parses = 0;
int invalid_parses = 0;
char key_buf[64];
char val_buf[64];
for (size_t trial = 0; trial < total_trials; trial++) {
size_t len = (size_t)(rand() % max_len);
char *buf = malloc(len + 1);
if (buf == NULL) return 2;
for (size_t i = 0; i < len; i++) {
/* Pilih karakter acak dari rentang terbatas termasuk '=' */
int r = rand() % 96; /* 0..95 */
buf[i] = (char)(' ' + r); /* range ' '..'~' sebagian */
}
buf[len] = '\0';
if (setjmp(fuzz_buf) == 0) {
int rc = parse_kv(buf, len, key_buf, sizeof(key_buf),
val_buf, sizeof(val_buf));
if (rc >= 0) valid_parses++;
else invalid_parses++;
} else {
printf("CRASH di trial %zu (len=%zu): ", trial, len);
printf("input=\"");
for (size_t i = 0; i < len && i < 20; i++) {
putchar(buf[i] >= ' ' && buf[i] <= '~' ? buf[i] : '.');
}
printf("\"\n");
crashes++;
if (crashes >= 5) { free(buf); break; }
}
free(buf);
}
printf("Total trials: %zu, valid: %d, invalid: %d, crashes: %d\n",
total_trials, valid_parses, invalid_parses, crashes);
return crashes != 0;
}Build dan jalankan:
gcc -std=c11 -Wall -Wextra -O0 -o fuzz fuzz.c parser.c && ./fuzzOutput yang Dihasilkan:
Total trials: 5000, valid: 1309, invalid: 3691, crashes: 0Mari kita telaah baris-baris kunci di atas:
- Baris 10 (
static jmp_buf fuzz_buf;): Buffersetjmpmenyimpan state register dan stack pointer. Saat crash,longjmpmengembalikan eksekusi ke titiksetjmp—seolah-olah fungsi yang crash baru saja kembali. - Baris 12-15 (
sig_handler): Handler menerima sinyal, mengabaikansig(parameter tidak dipakai dengan(void)sig), lalu memanggillongjmp. Trik ini mengubah crash menjadi recoverable error—kita bisa mencatat input penyebab dan melanjutkan loop. - Baris 24-26 (
signal(SIGSEGV, sig_handler); ...): Mendaftarkan handler untuk tiga sinyal crash umum.SIGSEGV= segmentation fault,SIGABRT=abort()(dipicuassertatau ASan),SIGFPE= pembagian dengan nol. - Baris 39 (
buf[i] = (char)(' ' + r);): Karakter acak dipilih dari rentang' '(0x20) hingga'~'+1. Rentang ini mencakup'='(0x3D) sehingga parser kadang menerima input valid. - Baris 47 (
if (setjmp(fuzz_buf) == 0)): Saat pertama kalisetjmpdipanggil, mengembalikan0—cabang normal dieksekusi. Bilalongjmp(fuzz_buf, 1)dipanggil dari handler,setjmpmengembalikan1—cabang crash dieksekusi. - Baris 59 (
if (crashes >= 5) { free(buf); break; }): Setelah 5 crash, loop berhenti agar output tetap terbaca. Untuk produksi, kita biasanya menyimpan input crash ke file dan melanjutkan fuzzing.
Mendeteksi Bug Memori dengan ASan #
Fuzz loop di atas mendeteksi crash yang menghasilkan sinyal. Namun, buffer overflow kecil yang tidak menyebabkan crash tetap berbahaya—data bisa terkorup tanpa terdeteksi. AddressSanitizer (ASan) memperluas deteksi: setiap akses memori diluar batas buffer langsung memicu abort(). Berikut parser buggy yang mengabaikan key_cap dan val_cap:
/* File: parser_buggy.c — BUG: tidak periksa kapasitas buffer */
#include "parser.h"
int parse_kv(const char *buf, size_t len, char *out_key, size_t key_cap,
char *out_val, size_t val_cap) {
(void)key_cap; (void)val_cap; /* BUG: argumen diabaikan */
if (buf == NULL || out_key == NULL || out_val == NULL) return -1;
size_t i = 0;
size_t k = 0;
while (i < len && buf[i] != '=') {
out_key[k++] = buf[i++]; /* BUG: tidak cek k >= key_cap */
}
out_key[k] = '\0';
if (i >= len) return -1;
i++;
size_t v = 0;
while (i < len) {
out_val[v++] = buf[i++]; /* BUG: tidak cek v >= val_cap */
}
out_val[v] = '\0';
return (int)i;
}Build dengan ASan dan fuzz loop dengan buffer key_buf[8] yang sengaja kecil:
gcc -std=c11 -Wall -Wextra -fsanitize=address \
-o fuzz_buggy fuzz_buggy.c parser_buggy.c && ./fuzz_buggyOutput yang Dihasilkan (dipotong untuk ringkas):
=================================================================
==9397==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f5b2fe00028
WRITE of size 1 at 0x7f5b2fe00028 thread T0
#0 0x... in parse_kv (/tmp/test_fuzz/fuzz_buggy+0x162f)
#1 0x... in main (/tmp/test_fuzz/fuzz_buggy+0x1439)
This frame has 2 object(s):
[32, 40) 'key_buf' (line 20) <== Memory access at offset 40 overflows this variable
[64, 72) 'val_buf' (line 21)
SUMMARY: AddressSanitizer: stack-buffer-overflow in parse_kvASan mendeteksi write ke out_key di luar batas key_buf[8]. Pesan menunjukkan bahwa akses terjadi pada offset 40, sedangkan key_buf hanya menempati offset 32-40—akses telah melampaui batas. Tanpa ASan, overflow ini mungkin tidak crash hingga bertahun-tahun kemudian, ketika data terkorup muncul sebagai bug misterius.
Strategi Fuzzing Lanjutan #
Fuzz loop di atas menggunakan input murni acak (black-box fuzzing). Pendekatan ini sederhana tetapi sering melewatkan input yang valid secara sintaks. Tiga pendekatan lanjutan:
- Grammar-based fuzzing: Generate input mengikuti grammar (misalnya
"KEY=VALUE\nKEY=VALUE\n..."), sehingga parser mencapai cabang dalam yang lebih dalam. - Mutation-based fuzzing: Mulai dari input valid (seed corpus), lalu lakukan mutasi kecil (flip bit, duplikasi segmen, hapus karakter) untuk menghasilkan variasi.
- Coverage-guided fuzzing: Alat seperti
libFuzzer(LLVM) atauAFL++memantau code coverage saat fuzzing dan memprioritaskan input yang mencapai cabang baru—lebih efisien daripada fuzz murni acak.