iddqd, atau jenis unsafe Rust yang paling sulit
(oxide.computer)- iddqd adalah pustaka map Rust yang meminjam kunci dari nilai, dan dipakai untuk mempertahankan indeks in-memory dari record besar seperti inventaris disk dan sled di control plane Omicron milik Oxide, sehingga ketepatan sangat penting
BTreeMapstandar menyimpan kunci dan nilai secara terpisah sehingga alur pemindahan data jadi merepotkan atau kunci duplikat bisa saling tidak cocok, tetapi IdOrdMap melakukan lookup dengan mengekstrak kunci dari field di dalam record- unsafe Rust adalah jalur keluar untuk mengekspresikan program aman yang tidak bisa dibuktikan oleh kompiler, dan kode generik harus tahan bahkan terhadap safe Rust yang patologis saat memanggil implementasi trait yang disediakan pengguna
- Iterasi mutable pada
iddqdmemperpanjang lifetime dengan bergantung pada invariant bahwa semua indeks berbeda, tetapi implementasi Ord yang patologis dapat membuat B-tree dan item set tidak sinkron sehingga indeks duplikat untuk item yang sama bisa tercipta - Perbaikannya membandingkan kunci dan indeks sekaligus, lalu jika gagal kembali ke pemindaian linear yang tidak memanggil kode pengguna, dan kepercayaan yang memadai hanya bisa didapat dengan menggabungkan Miri, pengujian berbasis model, panic fault injection, dan review adversarial oleh LLM
Masalah yang diselesaikan iddqd
iddqdadalah pustaka Rust yang menyediakan map dengan kunci yang dipinjam dari nilai, dan Oxide menggunakannya secara luas di control plane Omicron- Omicron adalah perangkat lunak di pusat rack Oxide yang memprovisikan resource seperti compute dan storage serta menjaga rack tetap berjalan, dan jika
iddqdsalah berfungsi, control plane bisa berperilaku salah dengan cara yang sulit diprediksi dan didiagnosis - Struktur seperti
BTreeMap<Email, User>di pustaka standar Rust menyimpan email sebagai kunci secara terpisah dari nilainya - Untuk mengoper kunci dan nilai bersama, kita harus mengambil
(email, user)denganget_key_value, dan jika membuat struktur terpisah sepertiUserRecordsaat fetch, pengelolaannya menjadi sulit ketika tipe record banyak - Jika email juga diduplikasi di dalam
User, muncul risiko email pada kunci map dan email di dalam nilai saling berbeda IdOrdMapmenggunakan traitIdOrdItemuntuk mengekstrak kunci dari record, dan tipe kunci bisa dipinjam dari nilai sepertitype Key<'a> = &'a Email- Di Oxide, pola ini cocok untuk record besar seperti hasil query database, dan berguna untuk pengindeksan record besar di seluruh control plane
Fitur tambahan
iddqdmendukung sebagai fitur kelas satu kunci kompleks yang dipinjam dari beberapa field, sehingga bisa ditangani tanpa jalan memutar seperti dynamic dispatchBiHashMapdanTriHashMapmasing-masing mengindeks satu item dengan dua atau tiga kunci, sehingga tidak perlu memakai pola umum berupa memelihara beberapa map secara manual- Implementasi Serde melakukan serialisasi sebagai sequence, bukan map, sehingga kunci non-string juga bisa diserialisasi dalam JSON, serta menolak kunci duplikat
- Demi kompatibilitas ke belakang, serialisasi dalam bentuk map juga didukung
Titik ketika unsafe Rust menjadi sulit
- Dalam Rust, risiko intinya adalah undefined behavior (UB); jika kode aman tidak mungkin menimbulkan UB dengan cara apa pun maka ia sound, jika tidak maka unsound
- Di dalam safe Rust, kompiler menolak program yang memiliki UB, tetapi karena keterbatasan analisis statis, sebagian program yang sebenarnya bebas UB juga ikut ditolak
- Kata kunci
unsafeadalah jalur keluar untuk mengekspresikan program semacam itu, dengan penulis mengambil tanggung jawab atas soundness dan meminta kompiler untuk mempercayainya - Aturan Rust yang harus dipatuhi kode unsafe mencakup larangan data race, larangan membaca memori yang belum diinisialisasi atau sudah dibebaskan, larangan beberapa alias
&mutpada area memori yang sama, dan larangan memodifikasi data immutable - Aturan ini sulit ditalar, terutama karena mutable aliasing, sehingga biasanya perlu mengenkapsulasi unsafe di balik abstraksi aman
Tahapan memverifikasi abstraksi aman
split_at_mutadalah metode aman untuk membagi mutable slice menjadi dua bagian, tetapi pembagian seperti ini tidak bisa diekspresikan hanya dengan safe Rust sehingga membutuhkan unsafe- Soundness
split_at_mutbergantung pada invariant di kode aman sekitarnya, seperti apakah ia menerima&mut [T], apakahmid <= slice.len()sudah diperiksa, dan apakah panjang sisanya dihitung dengan tepat getpadaMyVec<T>bergantung pada kondisi bahwalenakurat dan indeks masih dalam batas, dan kondisi ini harus dijaga oleh semua metode lain dalam modul yang sama- Tingkat kesulitan paling tinggi muncul ketika kode unsafe generik memanggil kode yang disediakan pengguna
- Kode safe Rust, betapapun patologis atau adversarial cara penulisannya, tidak boleh membuat kode unsafe menimbulkan UB
- Kode seperti
collect_exactyang mempercayailen()dariExactSizeIteratordan menulis sebanyak capacity umumnya unsound, karena jika iterator mengembalikan panjang palsu, ia bisa menulis ke memori yang tidak dialokasikan std::io::Readjuga merupakan trait yang implementasinya bisa buggy dan salah melaporkan jumlah byte yang dibaca, dan Rust RFC 2930 membahas masalah iniiddqdtermasuk dalam kategori tersulit ini karena merupakan struktur data generik yang memanggil implementasi trait yang disediakan pengguna
Struktur internal iddqd
iddqdmenggabungkanItemSet, yaitu daftar item, dengan tabel yang menyimpan indeks slotIdHashMap<T>menggunakanItemSet<T>danhashbrown::HashTableyang menyimpanItemIndexItemSet<T>memilikiVec<ItemSlot<T>>, danItemSlot<T>berbentukOccupied(T)atauVacant { next: ItemIndex }free_headmenunjuk ke slotVacantyang paling baru dibebaskan atau ke sentinel yang berarti tidak ada free slot, danfree_headbersama slot-slotVacantmembentuk free chain- Saat menyisipkan item baru,
free_headdiperiksa untuk melihat apakah ada slot yang bisa dipakai ulang; jika ada, slotVacantditimpa menjadiOccupiedlalufree_headdipindah ke nilai berikutnya - Jika tidak ada free slot, slot
Occupiedbaru di-pushke akhir, lalu kuncinya diambil, hash dihitung, dan indeks baru dicatat di hash table - Penghapusan bekerja sebaliknya: mencari indeks di hash table lewat kunci lalu menghapusnya, kemudian
ItemSlotterkait diubah menjadiVacantdanfree_headlama dihubungkan sebagainext IdOrdMapmemakai indeks B-tree alih-alih hash table, sedangkanBiHashMapdanTriHashMapmasing-masing menyimpan dua dan tiga hash table
Iterasi mutable dan perpanjangan lifetime
IdOrdMap::iter_mutmelakukan iterasi mutable atas item menurut urutan kunci- Iterator Rust tidak boleh mengembalikan nilai yang masih meminjam iterator itu sendiri, dan kombinator seperti
collectbisa menyisakan nilai sepertiVec<&mut T>bahkan setelah iteratornya hilang - Agar perilaku ini aman, iterator tidak boleh mengembalikan nilai yang sama dua kali
- Borrow checker hanya melihat akses berurutan terhadap daftar dan tidak bisa mengetahui bahwa semua indeks pasti berbeda
iddqdmemakai perpanjangan lifetime denganstd::mem::transmute::<&mut T, &'a mut T>, yang bergantung pada invariant bahwa indeks yang dikembalikanself.itersemuanya berbeda
Bug yang dibuat oleh implementasi Ord patologis
- Dalam
IdOrdMapberisi lima item dengan kunci 0 sampai 4 secara berurutan, jika kunci 0 diakses lewatEntry API, makaOccupiedEntrymenyimpan indeks 0 di dalamnya - Jika setelah itu implementasi
OrdpadaKeydiubah sehingga selalu mengembalikanEqualtanpa memandang nilainya,OccupiedEntry::removesaat menelusuri ulang B-tree bisa menghapus item yang salah hanya berdasarkan perbandingan kunci - Misalnya jika B-tree lebih dulu membandingkan
(key = 2, index = 2), entri itu akan dihapus karenaEqual, dan karenaOccupiedEntrymemegang indeks 0, item 0 akan dihapus dari item set - Dalam keadaan ini, indeks 0 masih tertinggal di B-tree tetapi slot 0 pada item set menjadi vacant, sementara item 2 tetap ada di item set namun kehilangan pointer B-tree
- Setelah
Ordkembali berfungsi normal dan item dengan kunci 1000 disisipkan, slot 0 yang ditunjukfree_headakan dipakai ulang - Hasilnya, indeks duplikat yang menunjuk ke slot yang sama muncul di B-tree, dan
IterMutbisa membuat beberapa referensi&mutke memori yang sama sehingga menjadi unsound
Cara perbaikan dan kompromi performa
- Saat menelusuri B-tree ke bawah, kini yang diperiksa bukan hanya kesetaraan kunci tetapi juga kesetaraan indeks
- Saat mencari kunci dengan indeks yang sudah diketahui, pencarian membandingkan
(key, index)sekaligus, sehingga meskipunOrdpatologis mengembalikanEqual, perbandingan antara(key = 2, index = 2)dan indeks 0 yang dicari akan menjadiLessberkat tie-breaker indeks - Agar pencarian ini berhasil, indeks yang tersimpan memang harus sama dengan indeks yang dicari
- Tie-breaker mencegah pencocokan item yang salah, tetapi tidak menjamin item yang benar selalu ditemukan
- B-tree diurutkan menurut kunci, sedangkan tie-breaker membandingkan indeks, sehingga kedua urutan ini pada umumnya saling independen
- Jika pencarian tree gagal, penghapusan indeks terkait di B-tree dilakukan dengan linear scan yang tidak memanggil kode pengguna
- Fallback ini memang mengubah operasi penghapusan dari waktu logaritmik menjadi linear, tetapi hanya tercapai bila ada kode pengguna yang buggy, sehingga dianggap kompromi yang dapat diterima
Lapisan verifikasi
- Karena
iddqddipakai sebagai struktur data fondasional, peninjauan analitis digabungkan dengan berbagai verifikasi empiris - Setiap blok unsafe dan pola unsafe dianalisis oleh satu sampai tiga penulis dan reviewer Rust
- Di atas setiap blok unsafe ada komentar
// SAFETY:yang mencatat penalarannya, dan lintundocumented_unsafe_blocksdari Clippy dipakai untuk memastikan komentarnya ada - Example-based test membuat map, menjalankan operasi, lalu memeriksa hasilnya;
iddqdmemiliki unit test internal dan integration test untuk API publik - Test-test ini juga ada sebagai doctest sehingga sekaligus berfungsi sebagai dokumentasi
- Test patologis memasok
Ordbuggy dan implementasi trait lainnya - Di CI, test reguler dan test patologis dijalankan di bawah Miri untuk mendeteksi berbagai jenis UB
- Kondisi seperti invariant bahwa tidak boleh ada indeks duplikat juga bisa diperiksa di lingkungan test biasa di luar Miri
Pengujian berbasis model dan fault injection
iddqdmemakai dua lapis randomized testing: model-based testing yang membandingkan dengan oracle yang benar, dan fault injection di atasnya- Model-based testing, atau stateful property-based testing, menerapkan urutan operasi acak pada sebuah instance tipe lalu membandingkan hasilnya dengan oracle yang diketahui benar
iddqdmenjalankan model-based test secara luas terhadapNaiveMaporacle yang tidak efisien tetapi jelas benar- Fault injection berarti menyuntikkan bug secara acak ke dalam kode pengguna, dan pada
iddqd, panic safety menjadi sumbu yang sangat efektif - Invariant map tidak boleh rusak meskipun kode pengguna panic di tengah operasi
- Setiap operasi map diberi panic countdown acak, lalu countdown dikurangi 1 setiap kali kode pengguna dipanggil, dan ketika mencapai 0 program panic, sehingga tercipta pengujian panic safety acak
- Pendekatan ini menemukan beberapa bug halus di
iddqd, dan sebagian di antaranya berujung pada unsoundness - Model-based test juga memverifikasi invariant internal seperti no-duplicate-index setelah setiap operasi
- Karena model-based test terlalu lambat untuk dijalankan di bawah Miri, invariant yang menjadi dasar soundness dan correctness diperiksa secara terpisah
Review adversarial oleh LLM dan teknik formal
- Model frontier generasi saat ini berhasil menemukan beberapa implementasi kode pengguna patologis yang dapat merusak map
- Dalam satu kasus yang menonjol, LLM menyusun jalur ketika custom allocator panic dan unwind, lalu invariant map rusak
- Ini adalah masalah panic safety yang berbeda dari panic kode pengguna biasa seperti implementasi
Ord, yang sudah dicakup oleh panic safety test sebelumnya - LLM juga bisa menghasilkan klaim soundness yang terlihat masuk akal tetapi salah, sehingga red-green TDD dengan Miri sebagai oracle menjadi mekanisme pertahanan
- Untuk bug soundness, langkahnya adalah terlebih dulu membuat test yang menunjukkan UB di bawah Miri, lalu setelah perbaikan memeriksa kembali bahwa test yang sama lolos
- Pendekatan untuk membuktikan invariant map dengan model checker seperti
Kaniterkendala karenaiddqdterlalu kompleks, sehingga proof yang dibutuhkan menjadi terlalu besar untuk ditangani alat tersebut Creusotdapat membantu membuktikan bahwa kode Rust bebas dari panic dan kesalahan lain, tetapi saat ini belum bisa membuktikan invariant yang harus tetap terjaga meskipun kode pengguna panic atau berperilaku salahNaiveMapberfungsi sebagai spesifikasi yang paling mendekati untukiddqd, dan CI menjalankan model-based test ribuan kali untuk memberi tingkat kepercayaan tinggi bahwa implementasinya sesuai dengan spesifikasicargo-annealadalah alat yang sedang dikembangkan dan menarik perhatian karena memungkinkan proof soundness Lean ditempatkan di komentar dokumentasi di samping unsafe Rustiddqdmemiliki invariant yang jelas dan cakupan yang terbatas tetapi tidak sepele, sehingga menjadi kandidat benchmark untuk alat verifikasi formal
Kesimpulan
- Unsafe generic Rust sangat sulit karena setiap invariant harus dipertahankan dengan mempertimbangkan implementasi trait apa pun, termasuk yang arbitrer dan adversarial
- Bug di
iddqdmenunjukkan bahwa implementasiOrdyang patologis bisa menipu map hingga menciptakan mutable alias pada memori yang sama - Tidak ada satu teknik pun yang dapat menangkap semua bug, sehingga penalaran unsafe baris demi baris oleh manusia, example-based test, pathological test, randomized test, dan review adversarial oleh model frontier dipakai bersama-sama
- Oxide menilai tingkat ketelitian seperti ini layak untuk infrastruktur fondasional
1 komentar
Komentar Lobste.rs
Kalau saya memahaminya dengan benar (sedang lihat di ponsel saat bepergian), ini tampaknya bisa diselesaikan dengan memakai tipe pembungkus dan
HashSet/BTreeSetalih-alihHashMap/BTreeMapTipe pembungkusnya sendiri tidak wajib, tetapi demi keamanan dan kemudahan pemeliharaan ke depan, itu pilihan yang baik
Yang dibutuhkan hanya pembungkus berukuran 0 di sekitar objek, lalu implementasi
PartialEq/Hashkustom yang hanya melihat field yang relevan. Jika ingin melakukan pencarian tanpa membangun nilai lengkap, Anda bisa membuat tipe kedua yang mengimplementasikanAsRefuntuk tipe nilaiPendekatan ini adalah cara untuk memakai ulang antarmuka
HashSet/BTreeSetyang ada apa adanya tanpaunsafe. Alih-alih membungkus tipe nilai/kunci, Anda juga bisa membuat pembungkusHashSet/BTreeSetbaru yang menangani perilaku semacam iniAPI
Entry, akses/iterasi yang dapat diubah, dan sebagainya juga ada. Di dalamiddqd, mutabilitas ditangani dengan cukup hati-hati, dan di beberapa tempat nilai atomik dipakai untuk mengizinkan interior mutability. Penanganan semacam ini akan cukup sulit tanpa indireksi indeksUntuk memverifikasi
iddqdsecara formal, awalnya saya rasa saya akan mencoba model checker seperti Kani untuk membuktikan bahwa peta tidak merusak invarian internal. Tetapi saya penasaran dengan bagian yang mengatakaniddqdsedikit terlalu rumit untuk ditangani Kani, dan bahwa pembuktian yang diperlukan menjadi terlalu besar untuk ditangani alat tersebutBisakah Anda berbagi lebih detail soal bagian ini? Saya penasaran apakah maksudnya kompleksitas komputasi algoritmenya mengalami degradasi ke kasus terburuk
Secara umum ini menarik sebagai studi kasus metode formal, dan saya senang bagian ini dibahas. Kalau melihat kisah sukses alat verifikasi formal yang ada pada proyek berskala besar, orang bisa secara naif mengira setidaknya kebenaran struktur data bisa dibuktikan, tetapi kenyataannya tingkat kesulitannya berbeda-beda untuk tiap struktur data, dan bahkan dalam bahasa yang dianggap lebih ramah pembuktian daripada bahasa seperti Rust yang mengizinkan aliasing tanpa batas, hal itu tetap tidak mudah secara praktis
Sedikit keluar topik, tapi saya juga penasaran bagaimana diagramnya dibuat. Apakah menulis skrip sekali pakai, karena itu terlihat seperti sesuatu yang dibuat khusus agar cocok dengan desain blog/situs Oxide dan bukan alat umum
Saya juga sempat mempersempit cakupannya. Misalnya, saya mengasumsikan
hashbrownbenar, tetapi itu pun tidak banyak membantu. Di titik itu saya nyaris menyerah. Untuk implementasi trait yang berfungsi baik, saya cukup yakiniddqditu benar, dan yang benar-benar saya minati dari metode formal adalah pembuktian yang tetap berlaku bahkan untuk implementasi arbitrer yang salahNamun saya juga bukan orang yang paling mahir memakai Kani. Akan sangat bagus kalau Anda atau orang lain mau mencoba ini
Untuk diagramnya, saya mula-mula membuat sketsa di Mermaid, lalu desainer kami yang luar biasa, Ben Leonard, menyempurnakannya secara manual menjadi diagram yang sesuai dengan palet warna dan tema kami
Pola peta berbasis field yang mengindeks objek memakai salah satu field objek itu sebagai kunci adalah sesuatu yang selalu saya harap bisa dipakai dengan mudah juga di C#
Dulu saya pernah mencoba membuat versi sederhana sendiri, tetapi antarmukanya tidak terlalu rapi jadi saya hentikan. Tulisan ini membuat saya ingin mencoba lagi
cargo-nextestsampai proyek pribadiMemakai satu field sebagai kunci memang kasus penggunaan yang paling umum, tetapi
iddqdcukup fleksibel untuk memakai kombinasi apa pun dari field, subfield, atau fungsi apa pun yang bisa dihitung dari field secara murni dan murah. Misalnya lihatArtifactKeydi https://docs.rs/iddqd/latest/iddqd/ (maaf kalau Anda tidak terbiasa dengan sintaks Rust)Saat merancang
iddqd, saya sangat merasa bahwa pengguna harus bisa memanfaatkan seluruh kekuatan sistem tipe Rust. Tidak masalah seberapa banyak jalan memutar yang harus saya tempuh sebagai penulis library. Saya benar-benar inginiddqdmenjadi library yang menyenangkan untuk dipakai para pengguna, terutama rekan kerja saya