Masalah use-after-free di sistem dengan object pool adalah salah satu bug yang paling sulit dilacak. Seorang developer yang menulis limit-order matching engine menceritakan pengalamannya: order yang dibatalkan dilepas kembali ke pool saat masih terhubung ke price level-nya, sehingga alokasi berikutnya memberikan memori itu ke order baru dan link lama terus ter-resolve. Awalnya dia menganggapnya sebagai kecerobohan soal lifetime. Tapi setelah membaca tulisan Matklad tentang memory safety, dia mulai ragu: apakah ini benar-benar soal lifetime, atau ada pola yang lebih dalam?
Matklad, engineer yang dikenal lewat karya di rust-analyzer dan TigerBeetle, menjawab pertanyaan ini dalam artikelnya Static Allocation, Constant Work. Jawabannya menarik: kasus object pool ini sebenarnya menjelaskan hubungan antara memory safety dan correctness secara lebih umum. Dan solusinya bukan cuma soal bahasa pemrograman, tapi soal keputusan desain arsitektur yang sering diabaikan.
Kenapa Object Pool Berbeda dari malloc/free Biasa
Tanpa object pool, kalau lo malloc dan free object order, kesalahan logis use-after-free berubah menjadi physical type confusion. Dua object dengan tipe berbeda berbagi lokasi memori yang sama, dan integer yang dikontrol pengguna di satu object bisa menjadi function pointer di object lain. Ini primitif goto yang bisa dieksploitasi, berpotensi menjadi arbitrary code execution.
Dengan object pool yang menyimpan daftar object mati bertipe T, situasinya berbeda. Use-after-free logis masih mungkin, tapi efek fisiknya berubah: masih ada aliasing memori, tapi tidak ada type confusion. Lo tidak bisa mengutak-atik integer dan mengubah function pointer, kecuali object yang bersangkutan menyimpan enum inline. Dengan asumsi itu tidak terjadi, perilakunya jadi terdefinisi dan deterministik, meski hasilnya mungkin tidak menyenangkan.
Ini wawasan penting: object pool yang menypisahkan tipe secara implisit mengubah bug memory safety yang eksploitable menjadi bug logika yang deterministik. Tidak keren, tapi jauh lebih mudah di-debug dan tidak bisa dieksploitasi menjadi code execution. Perbedaan ini sering tidak disadari developer yang langsung menyamakan semua use-after-free dengan kerentanan kritis.
Dari wawasan ini muncul ide menarik untuk hardening: kalau fungsi alokasi lo bertipe, yaitu menerima parameter tipe T atau type witness di runtime, bukan ukuran dan alignment yang type-erased, lo bisa menulis allocator yang memakai type-segregated pools secara internal. Alokator ini tidak akan bisa memakai ulang memori yang dibebaskan object tipe U untuk object tipe T, jadi sedikit kurang efisien, tapi overhead-nya biasanya kecil: tipe object yang jarang tidak masalah, tipe yang populer akan banyak reuse intra-tipe. Bonusnya, lo mungkin dapat locality memori yang lebih baik, dan menyelesaikan sebagian besar type confusion.
Fil-C, proyek yang disebut Matklad, tidak bisa memakai pendekatan ini karena interface allocator C tidak bertipe. Tapi bahasa lain yang punya allocator bertipe bisa menerapkannya. Ini contoh bagaimana pemikiran tentang memory safety mengarah ke keputusan desain allocator yang lebih cerdas, bukan cuma soal bahasa pemrograman.
Trik Pertama: Static Allocation
Solusi pertama yang dibagikan Matklad dari TigerStyle, style guide TigerBeetle, adalah: tidak ada alokasi memori dinamis setelah inisialisasi. Ide ini adalah kelanjutan logis dari konsep pool. Tentukan jumlah maksimum order yang mau ditangani di awal, dan jangan pernah melampauinya.
Secara praktis, lo menjalankan program dengan parameter seperti order-engine --orders-max=1_000_000, lalu di awal fungsi main mengalokasikan seluruh array order sekaligus. Kalau di runtime permintaan melebihi batas, permintaan surplus ditolak. Seseorang mungkin keberatan: bagaimana kalau sebenarnya masih ada memori kosong untuk satu order lagi? Bukankah lebih baik mencoba menanganinya?
Jawaban Matklad tajam: bagaimana kalau ternyata tidak? Sistem yang beroperasi di kapasitas penuh tanpa batas ketat gagal secara katastropik. Mencoba mengalokasikan satu Order lagi bisa memicu OOM killer kernel yang mematikan seluruh matching engine, menghilangkan satu juta order lain, atau lebih buruk lagi, membunuh proses supervisor sehingga lo bahkan tidak bisa me-restart.
Alokasi statis memberi ketenangan pikiran. Sistem mungkin gagal start kalau memori tidak cukup, tapi kalau sudah start, lo bisa yakin sistem akan menangani overload dengan anggun, terus melayani sambil lo menyiapkan mesin yang lebih besar. Untuk sistem trading, pembayaran, atau infrastruktur kritis lain, prediktabilitas semacam ini jauh lebih berharga daripada kemampuan menangani lonjakan kecil di luar batas.
Konservasi Jumlah Object
Matklad lalu membawa ide alokasi statis lebih jauh dengan konsep yang menarik: alih-alih memikirkan batas jumlah order, desain sistem supaya selalu punya jumlah order tetap, dengan memperkenalkan order netral yang tidak melakukan apa-apa. Order reserved dengan id nol, price nol, count nol, dan tag reserved. Inisialisasi jadi mengisi seluruh array dengan order reserved.
Manfaatnya pertama-tama kognitif. Lo tidak lagi berpikir dalam kerangka membuat dan menghancurkan order. Order hanya bersirkulasi dalam sistem mengikuti hukum kekekalan jumlah order. Menjadi lebih sulit kehilangan jejak order karena lo harus selalu memperhatikan bukan cuma ke mana order pergi, tapi juga dari mana asalnya. Lo secara eksplisit menulis fungsi transisi state untuk setiap pasangan state, dan itu memudahkan enumerasi semua kasus secara menyeluruh.
Manfaat kedua adalah kesederhanaan dan prediktabilitas kode. Lo tidak perlu melacak koleksi terpisah dari order yang hidup. Sebaliknya, lo selalu mengiterasi seluruh himpunan, melakukan no-op untuk order reserved. Ini terasa boros: bukankah kode harusnya jalan lebih cepat kalau ordernya sedikit?
Tapi pertimbangkan ini: dengan menentukan batas order di awal, lo berkomitmen untuk bisa melayani jumlah itu. Kalau jumlah maksimum order aktif dan performa sistem tidak acceptable, itu bug! Gray failure, yaitu sistem yang menjadi sangat lambat sehingga tidak berguna, adalah cara lain untuk gagal saat mencapai batas. Menghindari index justru meningkatkan performa untuk kasus beban maksimal. Iterasi langsung atas array order jauh lebih mudah divectorize oleh compiler dan lebih mudah di-prefetch oleh CPU cache daripada iterasi atas daftar index order aktif.
Trik Kedua: Constant Work
Trik kedua dari TigerStyle adalah jumlah kerja konstan per operasi. Ini tentang menghindari operasi yang biayanya tidak terduga di jalur kritis. Dalam konteks matching engine, maksudnya operasi seperti acquire dan release order harus punya kompleksitas yang bisa diprediksi, tidak bergantung pada keadaan sistem yang berubah-ubah.
Dengan alokasi statis sebagai fondasi, pool bisa diimplementasikan dengan struktur sederhana: array order yang dialokasikan di awal, plus struktur pelacak object kosong. Matklad menunjukkan dua pendekatan: DynamicBitSet yang melacak slot kosong, atau free list. Keduanya memberi operasi acquire dan release dengan biaya konstan atau mendekati konstan.
Sama seperti alokasi statis, prinsip constant work memberi ketenangan pikiran soal performa. Latensi P100, atau persentil ke-100, tetap datar terlepas dari beban. Performa yang tidak memadai ketahuan saat lo meluncurkan sistem, bukan saat on-call di tengah Black Friday. Ini kontras dengan sistem yang performanya menurun drastis di beban puncak, yang baru ketahuan di saat paling buruk.
Di TigerBeetle, pola ini diterapkan bahkan dalam skala kecil. Alih-alih menulis search loop dengan early return, mereka kadang membiarkan loop berjalan sampai selesai secara alami, sambil meng-assert bahwa ada tepat satu item yang cocok. Ini menghilangkan branch dari hot path dan membuat perilaku lebih mudah diverifikasi. Bukan teknik yang cocok untuk semua situasi, tapi berguna untuk dimiliki di arsenal.
Relevansi untuk Sistem di Indonesia
Pola pikir TigerStyle ini relevan untuk developer di Indonesia yang membangun sistem pembayaran, e-wallet, antrian, atau layanan keuangan. Sistem seperti ini sering menghadapi lonjakan beban yang tajam, misalnya saat promo besar atau akhir bulan. Kalau arsitekturnya bergantung pada alokasi dinamis tanpa batas, lonjakan bisa memicu kegagalan berantai yang sulit dipulihkan.
Alokasi statis dengan batas eksplisit mungkin terasa membatasi di awal. Tapi batas itu sebenarnya memberi kontrak yang jelas: sistem tahu persis kapasitasnya, dan bisa menolak dengan anggun, graceful rejection, daripada ambruk total. Dalam sistem pembayaran, menolak transaksi dengan pesan jelas jauh lebih baik daripada hang atau crash yang membuat pengguna tidak tahu status transaksinya.
Prinsip constant work juga penting untuk API yang melayani banyak klien. Kalau ada satu endpoint yang biayanya membengkak seiring jumlah data, endpoint itu akan jadi bottleneck yang tidak terduga. Mendesain agar setiap permintaan punya jumlah kerja yang terikat dan bisa diprediksi membuat kapasitas sistem mudah dihitung dan di-scale. Ini terutama relevan di Indonesia di mana traffic mobile sering melonjak tajam di jam-jam tertentu.
Kapan Prinsip Ini Tidak Cocok
Penting untuk diingat bahwa static allocation dan constant work bukan solusi universal. Matklad sendiri mengingatkan bahwa ini trik yang berguna di arsenal, tapi bukan jawaban untuk semua masalah programming. Untuk aplikasi dengan pola penggunaan yang sangat bervariasi dan tidak bisa diprediksi batas atasnya, alokasi statis bisa memaksa lo menyediakan kapasitas yang jarang terpakai.
Ada juga trade-off dalam hal fleksibilitas. Sistem dengan alokasi statis lebih sulit diubah untuk menangani skenario baru yang butuh lebih banyak resource. Kalau kebutuhan berubah drastis, lo harus mengubah batas di awal dan redeploy, bukan sekadar membiarkan sistem menyesuaikan diri. Untuk startup yang masih mencari product-market fit, fleksibilitas ini kadang lebih berharga daripada prediktabilitas.
Kuncinya adalah memahami konteks. Untuk sistem yang tidak boleh gagal dan punya batas beban yang bisa diperkirakan, seperti matching engine, database pembayaran, atau core banking, prinsip TigerStyle sangat berharga. Untuk aplikasi biasa dengan beban yang tidak menentu, pendekatan konvensional dengan autoscaling mungkin lebih masuk akal. Keputusan desain yang baik selalu bergantung pada kebutuhan spesifik sistem.
Kesimpulan
Tulisan Matklad tentang static allocation dan constant work menawarkan dua prinsip desain yang jarang dibahas di era cloud yang serba dinamis: batasi alokasi di awal, dan jaga biaya operasi tetap konstan. Keduanya berasal dari TigerBeetle, database yang dirancang untuk sistem pembayaran yang menuntut prediktabilitas ekstrem.
Konsep konservasi jumlah object, di mana order tidak pernah dibuat atau dihancurkan tapi hanya bersirkulasi, adalah cara berpikir yang elegan untuk menghindari seluruh kelas bug yang berhubungan dengan lifecycle object. Bagi siapa pun yang membangun sistem yang tidak boleh ambruk, dari matching engine sampai API pembayaran, dua trik ini layak diadopsi.
Bukan karena terdengar keren, tapi karena mereka mengubah bug memory safety yang eksploitable menjadi masalah logika yang deterministik, dan mengubah kegagalan katastropik menjadi penolakan yang anggun. Di dunia di mana sistem keuangan makin digital dan kegagalan sistem berarti kerugian nyata, prediktabilitas adalah fitur, bukan batasan.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬