11 year old unsoundness bugs found in Rust borrow checker

Jaminan paling mendasar yang ditawarkan Rust kepada ekosistem software engineering modern bertumpu pada satu premis: jika sebuah kode lolos kompilasi tanpa blok unsafe, program tersebut dijamin bebas dari Undefined Behavior (UB), use-after-free, dan data race.

Penemuan bug unsoundness yang telah bersarang selama 11 tahun di dalam core borrow checker rustc meruntuhkan ilusi absolutisme tersebut. Masalah ini bukan sekadar diskusi teoritis di forum akademik, melainkan sebuah bukti empiris bahwa sistem tipe paling mutakhir sekalipun rentan terhadap keterbatasan arsitektur kompilator.

Bagi backend engineer yang mengandalkan Rust untuk infrastruktur kritis, ini adalah momen untuk mengevaluasi kembali bagaimana abstraksi safety bekerja di level mesin.

---

Anatomi Masalah: Bagaimana Unsoundness Menembus Borrow Checker

Dalam terminologi sistem tipe, soundness berarti kompilator tidak akan pernah memvalidasi program yang salah (tidak ada false negative dalam deteksi error memori). Jika ada celah di mana safe code dapat memicu memory corruption tanpa bantuan unsafe, kompilator tersebut dinyatakan unsound.

Sebagian besar bug unsoundness jangka panjang di Rust berakar pada titik temu antara tiga komponen kompleks:

  1. Higher-Ranked Trait Bounds (HRTB) menggunakan sintaks for<'a>.
  2. Lifetime Variance dan Subtyping (bagaimana kompilator menentukan apakah &'a T kompatibel dengan &'b T).
  3. Trait Resolution dan Normalisasi Associated Types.

[Source Code] │ ▼ [HIR] │ ▼ [MIR] ──► [Region Inference (NLL / Constraint Graph)] ◄── Titik Lemah │ ▼ [Codegen / LLVM]


Awalnya, Rust menggunakan borrow checker berbasis AST (Abstract Syntax Tree) yang sangat restriktif dan kaku. Pada 2018, tim Rust beralih ke Non-Lexical Lifetimes (NLL) berbasis MIR (Mid-level Intermediate Representation). NLL menyelesaikan banyak masalah keterbatasan ekspresi kode, namun implementasinya masih menggunakan pendekatan berbasis *constraint graph* yang dibangun di atas serangkaian heuristik untuk efisiensi kompilasi.

Di balik layar, NLL memperlakukan lifetime sebagai kumpulan titik kontrol aliran (*points in the control-flow graph*). Namun, mesin pemeriksa kesetaraan tipe (*type equality engine*) dan *trait solver* (Chalk/old solver) beroperasi secara terpisah dari borrow checker.

Ketika berhadapan dengan ekspresi generik tingkat tinggi, pipeline ini mengalami apa yang disebut *leaky abstraction*:
* Trait solver mengasumsikan inferensi region akan memvalidasi batas memori.
* Region inferencer mengasumsikan trait solver telah memastikan proyeksi tipe valid secara semantik.

Celah di antara kedua asumsi inilah yang dieksploitasi oleh bug tersebut. Alasan utama kode ini bertahan 11 tahun adalah sifat sintaksisnya. Pola kode yang memicu kondisi perlombaan tipe (*type racing*) ini membutuhkan kombinasi trait bounds melingkar yang hampir tidak pernah ditulis secara natural oleh engineer manusia dalam use case komersial biasa.

---

### Trade-Off Arsitektural: Correctness vs Compile-Time vs Ergonomics

Mengapa tim rust-lang tidak langsung memperbaikinya begitu ditemukan? Di sinilah trade-off rekayasa perangkat lunak skala besar terjadi.

#### 1. Performa Kompilator (Compile Times)
Pemeriksaan formal yang 100% sound secara matematis untuk sistem tipe sekompleks Rust mendekati ranah kalkulasi NP-hard. Setiap penambahan pemeriksaan relasi lifetime pada MIR menambah latensi pada fase borrow checking. Kompilasi Rust sudah terkenal lambat; menambah overhead traversal graf lifetime yang terlalu dalam akan memperburuk developer experience secara masif.

#### 2. Risiko Breaking Changes (The Crater Impact)
Memperbaiki bug unsoundness sering kali membutuhkan pengetatan aturan subtyping. Masalahnya, ekosistem crates.io sangat masif. Ketika kompilator dibuat lebih ketat, ribuan crate legal yang aman secara operasional namun memiliki pola ketikan batas ambigu bisa mendadak gagal dikompilasi (*false positives*). Tim rust-lang harus menjalankan *crater run* (uji coba build ke seluruh ekosistem crates.io) sebelum berani menutup celah semacam ini.

#### 3. Polonius sebagai Solusi Hakiki
Arsitektur borrow checker saat ini (NLL) telah mencapai batas kemampuannya. Solusi jangka panjangnya adalah **Polonius**, borrow checker generasi berikutnya yang berbasis Datalog. Polonius memodelkan relasi peminjaman sebagai fakta deklaratif (*origin-based analysis*).

Namun, mengintegrasikan Polonius membutuhkan refaktor total pada sistem trait solver Rust (Next-Gen Trait Solver) dan saat ini biaya performa kompilasinya masih belum layak untuk production default.

---

### Implikasi untuk Backend Engineering di Production

Apakah server backend Anda yang berjalan di atas Rust (Axum, Actix, Tokio) rentan terhadap eksploitasi remote memory corruption saat ini? Secara praktis: **hampir pasti tidak.**

Bug unsoundness tipe ini bukan jenis bug yang bisa disuntikkan melalui payload HTTP atau deserialisasi JSON biasa. Penyerang harus bisa menyuntikkan kode Rust mentah ke dalam pipeline kompilasi Anda.

Namun, ada pelajaran arsitektural penting yang wajib dicatat:

#### Safe Rust Adalah Kontrak Praktis, Bukan Kebenaran Mutlak
Banyak engineer memperlakukan safe Rust seperti jaminan matematis yang absolut. Kenyataannya, kompilator adalah perangkat lunak biasa yang memiliki ratusan ribu baris logika imperatif dan bug tersembunyi. Keamanan memori adalah spektrum mitigasi risiko, bukan jaminan teologis.

#### Bahaya Macro Boilerplate
Meskipun engineer jarang menulis kode anomali ini secara manual, macro kompleks (seperti procedural macro untuk ORM, serialization, atau dependency injection) sering kali men-generate kode generik tingkat tinggi dengan trait bounds bersarang yang ekstrem. Di sinilah potensi memicu celah borrow checker meningkat tanpa disadari.

---

### Takeaways Konkret untuk Tim Engineer

1. **Jangan Turunkan Standar Pertahanan Berlapis (Defense-in-Depth):**
   Fakta bahwa aplikasi ditulis dalam Safe Rust tidak menghapuskan kebutuhan akan mitigasi tingkat OS. Tetap jalankan biner backend Anda dengan proteksi standar: ASLR aktif, alokator yang tahan eksploitasi (seperti hardened `jemalloc` atau `mimalloc`), dan sandbox berbasis seccomp/container dengan *least privilege*.

2. **Audit Macro Generatif dalam Basis Kode:**
   Jika arsitektur backend Anda sangat bergantung pada dynamic typing emulation di Rust menggunakan HRTB bersarang dan generic associated types, lakukan review berkala. Pastikan macro tidak menghasilkan struktur trait yang memaksakan inferensi lifetime berulang tanpa batas yang jelas.

3. **Monitor Migrasi Trait Solver Rust:**
   Ikuti perkembangan migrasi `-Znext-solver` dan Polonius di roadmap Rust. Transisi arsitektur ini nantinya akan membawa breaking changes kecil namun krusial pada kode yang bermain di batas sistem tipe.

4. **Sikap Kritis Terhadap Unsafe Boundaries:**
   Banyak tim backend mengaudit setiap baris blok `unsafe` dengan asumsi bahwa safe code di sekitarnya kebal terhadap kerusakan memori. Ketahuilah bahwa safe code yang mengonsumsi output dari inferensi compiler yang cacat tetap bisa memicu memory violation pada safe boundary tersebut.

Kekuatan Rust bukan terletak pada ketiadaan bug secara absolut di kompilatornya, melainkan transparansi tim intinya dalam mendokumentasikan, mengisolasi, dan secara konsisten mendesain ulang arsitektur internal kompilator guna menambal celah fundamental tersebut secara sistematis.

## Arsitektur & Alur Visual Sistem

![Arsitektur dan Alur Sistem: 11 year old unsoundness bugs found in Rust borrow checker](https://images.unsplash.com/photo-1542831371-29b0f74f9713?auto=format&fit=crop&w=1600&q=80)