Bahaya Abstraksi Hidden Header pada Deserialisasi API
Dalam ekosistem C, pola stretchy buffer atau dynamic array tanpa struct sering digunakan demi menyederhanakan sintaksis penulisan. Teknik ini memanfaatkan alokasi memori berlebih di depan array untuk menyimpan metadata ukuran (length) dan kapasitas (capacity). Fungsi atau makro kemudian mengembalikan pointer langsung ke elemen data pertama, sehingga array dapat diakses menggunakan operator indeks standar arr[i] seolah-olah merupakan array statis.
Ketika diterapkan pada pipeline deserialisasi batch API backend (misalnya memproses array JSON atau payload protobuf ratusan ribu item), trik ini menimbulkan kerentanan arsitektural fatal. Backend API beroperasi di atas input yang tidak tepercaya. Mengandalkan makro implisit yang melakukan realloc di balik layar saat mengurai batch item mengekspos sistem ke risiko pointer invalidation, kebocoran memori parsial saat parsing gagal, serta kerusakan memori saat data dilewatkan ke Foreign Function Interface (FFI).
Anatomi Masalah: Pointer Invalidation dan FFI Boundary
1. Pointer Invalidation Akibat Realloc Implisit
Makro dynamic array tanpa struct biasanya menyembunyikan logika ekspansi memori. Ketika kapasitas penuh, makro menjalankan realloc secara otomatis. Masalah muncul ketika deserializer memproses payload hierarkis di mana objek anak atau referensi lokal menyimpan alamat salah satu elemen array yang telah dialokasikan sebelumnya.
// Contoh skenario malapetaka pointer invalidation
Item *items = NULL;
for (size_t i = 0; i < payload_count; i++) {
buf_push(items, parse_item(stream)); // realloc terjadi di dalam makro
Item *last = &items[i];
// Jika payload memicu realloc berikutnya pada iterasi lanjutan,
// pointer 'last' atau pointer anak yang menunjuk ke memori lama
// seketika menjadi dangling pointer (Use-After-Free).
}Jika fungsi realloc memindahkan blok memori ke alamat virtual baru, semua pointer internal yang menunjuk ke blok lama menjadi tidak valid, menyebabkan korupsi data laten atau segmentasi fault mendadak di bawah beban kerja tinggi.
2. Kegagalan FFI (Foreign Function Interface) Boundary
Runtime bahasa modern seperti Rust, Go, Python (ctypes), atau C# mengasumsikan representasi memori C ABI standar untuk array kontinu: pointer dasar, panjang elemen, dan kapasitas. Mengirimkan pointer array bertrik hidden header ke runtime lain memicu undefined behavior:
- Jika runtime penerima mencoba memanggil
free(ptr)standar, allocator akan mengalami crash atau heap corruption karena pointer tidak menunjuk ke awal blok alokasi asli (offset header terlewatkan). - Jika runtime penerima mencoba memodifikasi ukuran array, runtime tidak mengenali metadata header kustom, merusak integritas pembacaan data di sisi C.
Perbandingan Kode: Hidden Header vs Explicit Struct Buffer
Berikut adalah perbandingan langsung antara pendekatan stretchy buffer makro dengan implementasi buffer eksplisit berbasis struct.
Anti-Pattern: Dynamic Array Menggunakan Hidden Header
#include <stdlib.h>
#include <stdio.h>
typedef struct {
size_t cap;
size_t len;
} ArrayHeader;
#define get_header(a) ((ArrayHeader *)(a) - 1)
#define arr_len(a) ((a) ? get_header(a)->len : 0)
#define arr_cap(a) ((a) ? get_header(a)->cap : 0)
void *arr_grow_internal(void *arr, size_t elem_size) {
size_t cap = arr_cap(arr);
size_t len = arr_len(arr);
size_t new_cap = cap ? cap * 2 : 8;
// Alokasi memori untuk header + elemen data
ArrayHeader *hdr = (ArrayHeader *)realloc(
arr ? get_header(arr) : NULL,
sizeof(ArrayHeader) + (new_cap * elem_size)
);
if (!hdr) return NULL;
hdr->cap = new_cap;
hdr->len = len;
return (void *)(hdr + 1); // Kembalikan pointer setelah header
}
#define arr_push(a, val) \
((!(a) || arr_len(a) >= arr_cap(a)) ? \
(*(void **)&(a) = arr_grow_internal((a), sizeof(*(a)))) : 0), \
(a)[get_header(a)->len++] = (val)Kelemahan kode di atas: jika arr_grow_internal gagal saat realloc, pointer asli (a) berisiko tertimpa nilai NULL sehingga memori awal bocor permanen (memory leak). Selain itu, pemanggil tidak memiliki kepemilikan siklus hidup memori yang jelas.
Pattern Rekomendasi: Kontrak Eksplisit Struct Buffer
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#define MAX_API_BATCH_SIZE 10000
typedef struct {
uint64_t id;
double value;
} Record;
typedef struct {
Record *data;
size_t len;
size_t cap;
} RecordBatchBuffer;
void record_batch_init(RecordBatchBuffer *buf) {
buf->data = NULL;
buf->len = 0;
buf->cap = 0;
}
void record_batch_free(RecordBatchBuffer *buf) {
if (buf->data) {
free(buf->data);
buf->data = NULL;
}
buf->len = 0;
buf->cap = 0;
}
bool record_batch_reserve(RecordBatchBuffer *buf, size_t new_cap) {
if (new_cap <= buf->cap) return true;
if (new_cap > MAX_API_BATCH_SIZE) return false; // Batas trust boundary
// Mencegah integer overflow pada perhitungan bytes
size_t bytes = 0;
if (__builtin_mul_overflow(new_cap, sizeof(Record), &bytes)) {
return false;
}
Record *tmp = (Record *)realloc(buf->data, bytes);
if (!tmp) return false; // Alokasi gagal, buf->data lama tetap aman
buf->data = tmp;
buf->cap = new_cap;
return true;
}
bool record_batch_push(RecordBatchBuffer *buf, Record item) {
if (buf->len >= buf->cap) {
size_t next_cap = buf->cap ? buf->cap * 2 : 16;
if (!record_batch_reserve(buf, next_cap)) return false;
}
buf->data[buf->len++] = item;
return true;
}Validasi Batas Payload dan Penanganan Error Tanpa Memory Leak
Saat memproses deserialisasi batch dari API stream, kegagalan parsing dapat terjadi di tengah-tengah pemrosesan (misalnya JSON terpotong atau format field tidak valid pada item ke-5.000). Dynamic array tanpa struct sering ditinggalkan dalam kondisi setengah teralokasi, menyulitkan pembersihan memori secara deterministik.
Pipeline deserialisasi API yang tangguh harus menerapkan dua aturan operasional:
- Upfront Limit Validation: Validasi parameter ukuran batch sebelum alokasi heap dilakukan. Jika API menyertakan header ukuran seperti
Content-Lengthatau fieldbatch_size, bandingkan nilainya dengan limit hard batas memori server. - Atomic Rollback pada Parsing Failure: Jika parsing elemen ke-N gagal, seluruh buffer harus dilepaskan seketika atau di-reset tanpa meninggalkan residu di heap.
int deserialize_batch_payload(const char *raw_stream, RecordBatchBuffer *out_buf) {
record_batch_init(out_buf);
size_t incoming_batch_count = get_batch_count(raw_stream);
// 1. Validasi trust boundary
if (incoming_batch_count > MAX_API_BATCH_SIZE) {
return -1; // Payload melampaui toleransi buffer
}
// 2. Pre-allocate memori sekaligus untuk memangkas overhead realloc berulang
if (!record_batch_reserve(out_buf, incoming_batch_count)) {
return -2; // Alokasi gagal
}
for (size_t i = 0; i < incoming_batch_count; i++) {
Record rec;
if (!parse_next_record(raw_stream, i, &rec)) {
// 3. Penanganan error tanpa leak: unwind buffer sepenuhnya
record_batch_free(out_buf);
return -3;
}
out_buf->data[out_buf->len++] = rec;
}
return 0; // Sukses
}Panduan Penerapan: Kapan Hidden Header Dilarang?
Meskipun dynamic array tanpa struct menawarkan kepraktisan sintaksis mirip bahasa tingkat tinggi, batas penggunaannya harus ditegaskan secara ketat dalam arsitektur backend C:
Dynamic array berbasis hidden header tidak boleh digunakan pada network boundary, endpoint API publik, integrasi library via FFI, maupun context multi-threading yang membagi pointer antar subsistem.
- Dilarang: Public API Deserializer. Mengurai batch API dari klien luar menuntut proteksi overflow memori dan penanganan
OOM(Out-Of-Memory) secara eksplisit. Makro hidden header cenderung mengaburkan error handling alokasi. - Dilarang: ABI Library Eksportir / FFI. Rust, Python, atau C# tidak dapat mengelola siklus hidup memori pointer C yang memiliki metadata tersembunyi di alamat negatif (
ptr - sizeof(Header)). - Dapat Ditoleransi: Internal Scratchpad & CLI Tools. Implementasi makro stretchy buffer dapat dipertahankan untuk manipulasi data internal yang terisolasi, skrip komputasi grafika, atau utilitas baris perintah di mana program segera melakukan terminasi setelah tugas selesai.
Menggunakan struktur data eksplisit struct { T *data; size_t len; size_t cap; } menjaga kode C tetap deterministik, mematuhi standar C ABI, dan mencegah eksploitasi keamanan berbasis korupsi heap.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!