11 year old unsoundness bugs found in Rust borrow checker

Ilusi Garansi Mutlak: Anatomi Bug Unsoundness 11 Tahun di Rust Borrow Checker

Klaim bahwa safe Rust menjamin ketiadaan undefined behavior (UB) secara absolut selalu menyisakan catatan kaki teknis: jaminan tersebut hanya sevalid implementasi type system dan borrow checker di dalam rustc. Penemuan bug unsoundness yang telah bersemayam selama 11 tahun di inti logika inferensi lifetime membuktikan catatan kaki tersebut.

Isu yang ramai diperdebatkan di komunitas teknis ini bukan sekadar edge case sepele. Celah ini memungkinkan kode safe Rust yang valid secara sintaksis untuk memalsukan relasi lifetime, memicu use-after-free, dan merusak integritas memori tanpa menggunakan satu pun blok unsafe.

Bagi backend engineer yang mengandalkan Rust untuk sistem konkurensi performa tinggi, insiden ini patut dibedah secara dingin: bagaimana arsitektur compiler membiarkannya lolos selama lebih dari satu dekade, apa kompromi di balik perbaikannya, dan bagaimana mitigasi nyatanya di lingkungan produksi.

---

Di Balik Layar: Interaksi Fatal Antara Subtyping, Variance, dan Trait Normalization

Inti dari bug ini berakar pada interaksi kompleks antara tiga pilar type system Rust: Higher-Ranked Trait Bounds (HRTB), aturan variance, dan cara compiler menormalisasi associated types di dalam konstrain generik.

Secara formal, borrow checker bekerja dengan membangun graf kendala (constraint graph) untuk setiap region memori ('a). Ketika Anda meminjam data, compiler menentukan apakah referensi tersebut bersifat covariant (bisa digantikan oleh lifetime yang lebih panjang) atau invariant (harus memiliki lifetime yang tepat sama, seperti pada &mut T).

// Representasi konseptual eksploitasi unsoundness:
// Memanipulasi variance melalui trait projection untuk memalsukan 'static

trait Trait<'a> {
    type Assoc;
}

impl<'a, T> Trait<'a> for T {
    type Assoc = &'a T;
}

fn bypass_lifetime<'a, T>(val: &'a T) -> <T as Trait<'static>>::Assoc {
    // Normalisasi trait solver gagal menegakkan invariansi region
    // Mengizinkan 'a diproyeksikan menjadi 'static tanpa verifikasi subtyping
    todo!()
}

Kegagalan ini terjadi ketika compiler mencoba menyederhanakan ekspresi tipe melalui trait solver. Dalam rantai inferensi:

  1. Subtyping Ambiguity: Compiler harus membuktikan bahwa tipe input memenuhi batasan untuk semua lifetime yang mungkin (for<'a>).
  2. Loss of Invariance: Selama proses normalisasi associated type, metadata terkait batas region temporer tereduksi sebelum borrow checker (khususnya implementasi berbasis MIR) memvalidasi graf kendala akhirnya.
  3. Manufactured 'static: Akibat reduksi yang terlalu dini tersebut, relasi subtyping yang seharusnya invariant diperlakukan sebagai covariant. Hasilnya, referensi dengan lifetime lokal dapat dikonversi menjadi referensi dengan lifetime yang lebih panjang, bahkan 'static.

Kode yang dihasilkan lolos tahap kompilasi dengan mulus. Namun, pada tahap runtime, pointer tersebut membaca alamat stack yang telah dialokasikan ulang untuk frame fungsi lain.

---

Akar Sejarah: Mengapa Bertahan Selama 11 Tahun?

Usia 11 tahun menempatkan asal-usul kode ini pada era pra-Rust 1.0 (sekitar tahun 2013-2014). Bertahannya cacat logika ini dipicu oleh tiga faktor arsitektural:

1. Transisi Borrow Checker yang Bertahap

Rust berevolusi dari AST-based borrow checker ke MIR-based borrow checker (dikenal sebagai Non-Lexical Lifetimes atau NLL pada 2018). Meskipun NLL memperbaiki presisi penanganan kontrol alur, fondasi trait solving dan region checking level rendah sebagian besar masih meminjam asumsi lama. Tim compiler memprioritaskan penyelesaian masalah ergonomi umum dibanding membongkar ulang algoritma inferensi formal.

2. Keterbatasan Chalk dan Transisi ke Next-Gen Trait Solver

Upaya memformalkan logika tipe Rust ke dalam logika orde pertama (proyek Chalk) memakan waktu bertahun-tahun dan tidak pernah sepenuhnya diadopsi secara penuh di rustc. Saat ini, compiler sedang berada di tengah migrasi besar menuju new trait solver (k-next solver). Celah 11 tahun ini berada tepat di area abu-abu antara trait engine lawas yang mengandalkan heuristik dan engine baru yang belum sepenuhnya matang.

3. Polonius Masih Menjadi Eksperimen

NLL memiliki kelemahan inheren dalam menangani pinjaman bersarang (nested loans) dan subtyping kompleks. Solusi jangka panjangnya adalah Polonius, model borrow checking berbasis deklaratif (Datalog). Karena Polonius belum dijadikan default akibat isu performa kompilasi, penambalan pada engine NLL saat ini ibarat menaruh plester di atas retakan struktural.

---

Trade-off Arsitektur: Presisi Teoretis vs Waktu Kompilasi

Memperbaiki bug semacam ini di level compiler bukanlah perkara mudah seperti menambahkan if/else. Ada trade-off besar yang harus dihadapi tim pengembang Rust:

  • Compilation Throughput vs Correctness:

Pengecekan subtyping yang fully-sound membutuhkan verifikasi penutupan transitif pada seluruh graf constraint tipe. Jika setiap dereferensi dan proyeksi trait harus menyelesaikan sistem persamaan logika formal, compile time yang selama ini dikeluhkan oleh engineer Rust backend akan melonjak drastis.

  • Backwards Compatibility (Hyrum’s Law):

Mengencangkan aturan trait bound berisiko memecahkan kode yang sebenarnya aman di ekosistem crates.io. Compiler sering kali mendapati bahwa memperbaiki sebuah unsoundness teoritis merusak ratusan library populer yang secara tidak sengaja bergantung pada celah fleksibilitas tersebut.

Biaya komputasi untuk mencapai kebenaran matematis absolut pada sistem tipe sekompleks Rust sangatlah mahal. Selama ini, tim compiler mengambil kompromi pragmatis: menutup celah yang umum ditemui, dan menunda restrukturisasi formal hingga engine baru siap. Bug ini membuktikan batas dari kompromi tersebut.

---

Implikasi di Production: Apakah Backend Anda Terancam?

Untuk sebagian besar backend engineer, jawabannya adalah: kemungkinan besar tidak secara langsung, namun batasan model ancaman Anda telah bergeser.

  • Zero-Day Exploit: Sangat tidak mungkin penyerang eksternal dapat mengeksploitasi bug ini melalui input jaringan biasa, kecuali sistem Anda mengompilasi dan mengeksekusi kode Rust pihak ketiga yang tidak tepercaya secara dinamis (dynamic code execution as a service).
  • Library Dependencies: Risiko terbesar berasal dari dependensi pihak ketiga. Library performa tinggi yang memanipulasi zero-copy deserialization, custom memory pool, atau lock-free data structures sering kali bermain di batas ekstrem sistem tipe. Bug ini membuka peluang kode yang terlihat aman di audit ternyata menyembunyikan korupsi memori.

---

Takeaways Konkret untuk Backend Engineer

Sebagai praktisi yang bertanggung jawab atas keandalan sistem di production, hilangkan asumsi bahwa safe Rust adalah dinding pertahanan tanpa celah. Langkah-langkah mitigasi konkret:

  1. Jangan Perlakukan Compiler sebagai Pengganti Fuzzing:

Sistem berbasis Rust tetap membutuhkan AddressSanitizer (ASan) dan fuzz testing (misalnya dengan cargo-fuzz), khususnya untuk pipeline yang memproses parsing protokol binary mentah. Jika ada UB yang lolos dari type checker, sanitizer runtime adalah jaring pengaman terakhir Anda.

  1. Audit Penggunaan Generik yang Terlalu Kompleks:

Hindari pola arsitektur kode internal yang menggunakan type-level trickery secara berlebihan hanya demi abstraksi estetis. Kompleksitas tinggi pada trait bounds berbanding lurus dengan kemungkinan Anda memicu sudut-sudut gelap trait solver rustc.

  1. Pantau Pinning Versi Toolchain:

Ketika fix untuk isu ini dirilis (kemungkinan besar memerlukan pengetatan aturan validasi lifetime), ekspektasikan adanya regresi kecil atau kode usang yang gagal dikompilasi. Uji build pipeline backend Anda pada branch beta/nightly secara berkala untuk mendeteksi breaking changes pada sistem tipe lebih awal.

  1. Pragmatisme di Atas Dogma:

Keunggulan Rust tetap valid: ia mengeliminasi 99% kelas bug memori yang biasa ditemukan di C/C++. Namun, memahami bahwa 1% sisanya dapat bersumber dari arsitektur internal compiler adalah pembeda antara developer pemula yang dogmatis dan senior engineer yang siap menghadapi realitas produksi.

Arsitektur & Alur Visual Sistem

Arsitektur dan Alur Sistem: 11 year old unsoundness bugs found in Rust borrow checker