coq-of-rust, yang memindahkan program Rust ke Coq, mulai menangani hingga crate core dan alloc dari standard library, sehingga mengurangi beban menulis definisi Coq secara manual untuk tiap fungsi primitive- Karena kedua crate ini adalah codebase besar yang berisi banyak kode unsafe dan Rust tingkat lanjut, tantangan utamanya adalah menangani hasil terjemahan otomatis sebagai unit yang bisa dikompilasi dan diverifikasi
- Setelah output dibagi per file Rust input,
allocmenjadi 54 file dengan 171.783 baris, dancoremenjadi 190 file dengan 592.065 baris, sehingga kompilasi paralel dan debugging menjadi lebih mudah - Konflik nama modul pada blok
impltelah dikurangi dengan menyertakan informasi klausawhere, tetapi file yang saat ini belum dapat dikompilasi di Coq masih tersisa 4% dari keseluruhan - Contoh
Option::unwrap_or_defaultmenggunakan pendekatan membuktikan ekuivalensi antara definisi terjemahan otomatis dan definisi fungsional yang sederhana, sehingga diperlukan kepercayaan pada otomatisasi sekaligus pemeriksaan pada saat pembuktian
Penanganan primitive standard library Rust
- Formal Land sedang mengembangkan
coq-of-rust, yang menerjemahkan program Rust ke sistem pembuktian formal Coq - Sebelumnya, untuk menangani komponen primitive dari standard library Rust, perlu dibuat definisi Coq terpisah yang merepresentasikan perilaku tiap fungsi
- Contoh:
Option::unwrap_or_default - Definisi manual bersifat repetitif dan mudah disusupi kesalahan
- Contoh:
- Untuk mengurangi beban ini, crate
coredanallocRust diterjemahkan dengancoq-of-rust - Hasil terjemahannya dapat dilihat di jalur berikut
Hasil eksekusi terjemahan awal
- Saat
coq-of-rustpertama kali dijalankan padaallocdancore, dihasilkan 2 file Coq berukuran ratusan ribu baris yang masing-masing mencakup keseluruhan crate - Terbukti bahwa tool tersebut bisa berjalan pada codebase besar, tetapi kode Coq yang dihasilkan belum langsung dapat dikompilasi
- Error muncul jarang, tetapi kira-kira pada tingkat satu error setiap beberapa ribu baris
- Menurut
cloc, ukuran kode Rust input adalah sebagai berikutalloc: 26.299 baris kode Rustcore: 54.192 baris kode Rust
- Karena ekspansi macro terjadi selama proses terjemahan, target terjemahan sebenarnya lebih besar daripada jumlah baris asli
Pemecahan kode Coq yang dihasilkan
- Perubahan terbesar adalah memecah output
coq-of-rustmenjadi satu file Coq untuk tiap file Rust input - Pemecahan ini dimungkinkan karena terjemahannya tidak sensitif terhadap urutan definisi dan bersifat context-free
- Biasanya ada dependensi siklik antarfile Rust dan Coq tidak mengizinkannya, tetapi dalam metode terjemahan ini pemisahan per file dimungkinkan
- Setelah pemecahan, ukuran output adalah sebagai berikut
alloc: 54 file Coq, 171.783 baris kode Coqcore: 190 file Coq, 592.065 baris kode Coq
- Dengan memecah file, eksplorasi dan pemeliharaan kode yang dihasilkan menjadi lebih mudah
- Lebih mudah memparalelkan kompilasi
- Bisa fokus melakukan debugging satu file demi satu file
- Lebih mudah mengecualikan file yang tidak dapat dikompilasi
- Lebih mudah melacak diff pada satu file
Perbaikan konflik nama modul dan file yang tersisa
- Beberapa bug muncul dari konflik nama modul pada blok
impl - Cara penyelesaiannya adalah memasukkan lebih banyak informasi ke nama modul untuk meningkatkan keunikannya
- Informasi klausa
whereyang sebelumnya hilang kini disertakan - Misalnya, pada implementasi trait
DefaultuntukMapping<K, V>, kondisi bahwaKdanVsama-sama harus mengimplementasikanDefaultdirefleksikan dalam nama modul
- Informasi klausa
- File yang saat ini tidak dapat dikompilasi di Coq adalah sebagai berikut
alloc/boxed.vcore/any.vcore/array/mod.vcore/cmp/bytewise.vcore/error.vcore/escape.vcore/iter/adapters/flatten.vcore/net/ip_addr.v
- Ini setara dengan 4% dari seluruh file
- Di dalam file yang dapat dikompilasi pun masih ada komponen Rust yang belum tertangani dan diaxiomatiskan, sehingga sulit menilai seluruh cakupan yang belum didukung hanya dari rasio ini
Contoh terjemahan Option::unwrap_or_default
Option::unwrap_or_defaultdi Rust mengembalikanxjikaSome(x), dan memanggilT::default()jikaNonecoq-of-rustmenerjemahkannya menjadi definisi Coq berbentuk monadic- Mencocokkan argumen input dan tipe
- Pada cabang
Some, mengambil tuple field lalu menyalinnya - Pada cabang
None, memanggil metodedefaultdari traitcore::default::Default
- Dalam verifikasi nyata, definisi fungsional yang lebih sederhana digunakan alih-alih definisi yang dihasilkan otomatis
- Jika
None, mengembalikancore.simulations.default.Default.default - Jika
Some x, mengembalikanx
- Jika
- Bukti bahwa definisi yang dihasilkan otomatis ekuivalen dengan definisi sederhana ada di
CoqOfRust/core/proofs/option.v - Jika kode Rust asli berubah, perubahan tersebut akan terdeteksi melalui bukti ini
- Karena penerjemahan library
coredilakukan secara otomatis, definisi yang dihasilkan dapat lebih dipercaya daripada definisi yang ditulis manual - Namun,
coq-of-rustjuga bisa memiliki kesalahan atau ketidaklengkapan, sehingga validitas kode perlu diperiksa pada saat pembuktian
Tugas yang tersisa
- Kepercayaan terhadap formalisasi standard library dalam verifikasi program Rust meningkat
- Target berikutnya tetap menyederhanakan proses pembuktian yang membosankan
- Secara khusus, untuk menunjukkan bahwa simulasi ekuivalen dengan kode Rust asli, diperlukan pekerjaan berikut
- Resolusi nama
- Pengenalan tipe tingkat tinggi
- Penghilangan efek samping
- Menangani tahap-tahap ini secara terpisah menjadi arah perbaikan ke depan
1 komentar
Komentar Hacker News
Benar-benar mengesankan
Terjemahan otomatis seperti ini memindahkan objek kepercayaan ke alatnya. coq-of-rust sendiri ditulis dalam Rust, bukan Coq, jadi struktur rekursifnya cukup mengejutkan. Namun jika pendekatan ala CompCert digabungkan dengan cara menghindari serangan Trusting Trust dari Ken Thompson menggunakan compiler kedua, seperti dalam “Countering Trusting Trust through Diverse Double-Compiling” (2009) karya David A. Wheeler [0], tampaknya pembuktian kebenaran bisa dilakukan
Untuk memverifikasinya, terjemahkan translator coq-of-rust dari Rust ke Coq menggunakan coq-of-rust. Karena penerjemahan itu dilakukan oleh Rust, meskipun kita tidak memercayainya, cukup buktikan di dalam Coq sifat kebenaran yang diinginkan, khususnya bahwa semantik dipertahankan saat menerjemahkan program Rust ke Coq
Seperti dalam makalahnya, kemungkinan akan lebih mudah membuktikan dengan definisi yang lebih fungsional daripada definisi yang dihasilkan, jadi tinggal melalui proses membuktikan ekuivalensi antar-definisi seperti yang dilakukan pada pustaka standar. Jika translator coq-of-rust saat ini, khususnya lib/ [1], berisi 6.350 baris Rust, menulis seluruh translator dalam Coq dan membuktikan bahwa ia ekuivalen dengan hasil yang dihasilkan juga terlihat realistis
Setelah itu, jika translator coq-of-rust versi Coq yang sudah terbukti dijalankan pada source coq-of-rust yang ditulis dalam Rust, definisi Coq yang keluar harus cocok dengan output translator coq-of-rust versi Rust yang digunakan pada awalnya
Sebagai catatan tambahan, menyenangkan melihat pekerjaan semacam ini mendapat pendanaan industri. Saya cenderung sinis terhadap kripto, tetapi memang benar bahwa kebutuhan mereka akan kebenaran mendorong peningkatan di bidang-bidang yang saya minati, seperti Rust, Coq, dan dukungan untuk mahasiswa magister yang saya kenal
[0]: https://dwheeler.com/trusting-trust/wheelerd-trust.pdf
[1]: https://github.com/formal-land/coq-of-rust/tree/main/lib
Namun meskipun kodenya sendiri pendek, ia bergantung pada compiler Rust untuk mem-parse dan melakukan type checking pada kode Rust input. Jadi bagian itu juga perlu diverifikasi, atau setidaknya diberi spesifikasi formal meski tanpa pembuktian. API rustc yang cukup besar dan tidak stabil menjadi hambatan, tetapi tetap bisa menjadi salah satu cara untuk meningkatkan tingkat kepercayaan
Tentu muncul masalah bahwa output atau converter-nya harus diverifikasi, tetapi kode C yang dihasilkan cukup mudah dibaca untuk tujuan verifikasi, gayanya juga terbatas, dan kepercayaan terhadap alatnya cukup tinggi. Analisis statis SPARK hanyalah salah satu bagian dari keseluruhan proses verifikasi dan validasi, sementara pengujian dan aktivitas lain menyediakan lapisan kepercayaan tambahan. Secara keseluruhan, pendekatan itu bekerja cukup baik
Mereka membelanjakan uang itu untuk riset ilmu komputer bukan hanya karena menguntungkan bagi mereka, tetapi juga karena itu merupakan kegiatan filantropi yang bersinggungan dengan hobi mereka
Pendekatan lain adalah proof-carrying code. Compiler Rust mengeluarkan pembuktian Coq bahwa output executable sesuai dengan semantik source code input
Tentu saja, bisa juga menulis compiler untuk subset Rust, misalnya tanpa borrow checker atau optimisasi, ke machine code arsitektur tertentu, lalu melakukan bootstrap semuanya dari sana
Ukuran program yang diverifikasi dari awal sampai akhir dengan sistem pembuktian deduktif semi-otomatis seperti Coq itu kecil. Pustaka mungkin lebih mudah karena tingkat interaksi antarkode lebih rendah, tetapi program umum tidak demikian
Dalam praktiknya, pertumbuhan ukuran program yang bisa diverifikasi dengan cara ini lebih lambat daripada pertumbuhan ukuran rata-rata seluruh program. Verifikasi perangkat lunak yang sound, yaitu menunjukkan bahwa ia 100% sesuai dengan spesifikasi, jelas berguna untuk hal-hal seperti pembuktian kebenaran algoritma inti, tetapi sulit diskalakan
Karena itu, sebagian riset bergeser ke metode yang tidak sound. Biaya jaminan 100% bisa 10 kali lipat dari jaminan 99,9999%, dan selisih probabilitas itu bisa lebih kecil daripada probabilitas kegagalan di luar perangkat lunak. Sistem fisik sejak awal tidak bisa dibuktikan sesuai dengan spesifikasi, dan spesifikasi itu sendiri juga bisa saja tidak cukup cocok dengan kenyataan
Membuktikan bahwa hanya kode unsafe yang benar membutuhkan usaha jauh lebih kecil daripada membuktikan semua kode Rust. Ini menjadi jawaban atas kritik yang sering muncul saat membicarakan jaminan keamanan Rust: “bagaimana dengan kode unsafe?” Celah yang tidak cukup kuat untuk ditangani hanya oleh type system Rust dapat diisi dengan sistem yang lebih kuat seperti Coq
Ada bagian yang sulit saya pahami dari upaya seperti ini. Jika kode harus dikonversi ke Coq secara manual atau semimanual, bukankah kemungkinan membuat kesalahan dalam proses itu jauh lebih besar daripada manfaat yang didapat dari verifikasi formal?
Dengan kata lain, bagaimana kita tahu bahwa apa yang kita buktikan masih berlaku untuk kode asli?
Verifikasi formal sering dibahas seolah-olah sepenuhnya menghilangkan kemungkinan bug, tetapi dalam praktiknya lebih tepat dikatakan sangat menurunkan kemungkinan bug. Meski begitu, penerjemahan otomatis antara Rust dan Coq sangat mengurangi kompleksitas hal-hal yang harus dipercaya, sehingga seharusnya lebih dipilih daripada penerjemahan manual
Dalam banyak kasus, bug dalam penerjemahan hanya membuat pembuktian menjadi tidak mungkin. Lalu seseorang akan menyelidiki mengapa pembuktian tidak lolos dan menemukan bug penerjemahan tersebut
Masalah sebenarnya adalah jika bug penerjemahan secara tepat meniadakan bug dalam kode asli. Jika tidak ada risiko sistematis, kemungkinan dua bug saling menghapus seperti ini cukup rendah
Untuk pembaca yang berminat: saya mengirimkan posting blog ini karena konten terkait kriptonya tidak terlalu langsung dibandingkan tulisan lain di blog yang sama, tetapi di sana ada banyak tulisan yang secara teknis lebih menarik
Khususnya, dua tulisan terbaru membahas penerapan pendekatan yang sama bukan ke Rust, melainkan ke Python
Saya teringat sebuah kuliah [1] tentang fuzzer yang dulu menemukan bug pada compiler C yang sudah diverifikasi secara formal. Penyebabnya adalah verifikasi formalnya tidak mencakup frontend dan backend
Saya juga paham munculnya pertanyaan seberapa jauh Coq itu sendiri atau penerjemahannya bisa dipercaya, dan saya juga bertanya-tanya bagaimana ini akan disinkronkan dengan pembaruan Rust, tetapi bahkan verifikasi formal yang sempurna pun tidak berarti akurasi 100% dari awal sampai akhir
[1] https://youtu.be/Ux0YnVEaI6A
Saya sama sekali bukan ahli verifikasi formal, tetapi jika library dasar Rust diverifikasi secara formal dan tidak memakai kode unsafe, apakah semua program Rust yang memakai library yang sudah diverifikasi secara formal itu pada dasarnya akan memiliki kualitas setingkat verifikasi formal dalam hal penanganan memori?
Rust punya definisi “safe” sendiri, dan ini bisa dianggap sebagai subset dari memory safety. Bahkan dalam kode Rust yang sepenuhnya safe, data race, deadlock, kehabisan memori, dan tentu saja kesalahan logika tetap mungkin terjadi
Pertama, semantik unsafe Rust harus diformalkan. Karya perintis RustBelt[1] dari Ralf Jung merupakan kemajuan besar, tetapi belum lengkap. Khususnya, asal-usul pointer ternyata menjadi unsur yang rumit
Kedua, sebagai bagian dari itu diperlukan model formal untuk borrow checker. Stacked borrows[2] adalah upaya yang baik tetapi memiliki cacat, dan Tree borrows[3] mungkin bisa memperbaikinya, meski bisa juga muncul sesuatu yang lebih canggih
Ketiga, diperlukan model memori formal. Ini terutama berkaitan dengan perilaku operasi atomik dan sinkronisasi, sehingga sangat penting bagi komponen standard library seperti Arc. Secara luas diterima bahwa model memori Rust harus mirip dengan C++ dan dapat berinteroperasi dengannya, tetapi masalah “out of thin air” dan fitur yang belum ada seperti seqlock masih tersisa. Itu salah satu alasan kernel Linux masih memakai modelnya sendiri
Keempat, perlu ada bukti bahwa jaminan keamanan dapat dikomposisikan dengan baik. Secara khusus, jaminan keamanan harus tetap terjaga ketika kode yang sound dan ditulis dengan unsafe Rust dikomposisikan dengan kode safe Rust. Sejauh ini hasilnya bagus, tetapi harus dibuktikan untuk keseluruhan sistem
Kelima, agar pembuktian seperti itu berlaku untuk semua kode, semua bug soundness[1] yang tersisa di Rust harus ditutup. Banyak dari masalah ini bersifat teoretis atau baru benar-benar penting pada kode adversarial[5], sehingga progresnya lambat
Bahkan setelah semua itu selesai, jaminannya tetap hanya parsial. Sebagian sangat besar dari permukaan yang berinteraksi dengan sistem didasarkan pada kode unsafe. Jika yang dilakukan hanya komputasi murni mungkin tidak masalah, tetapi misalnya jika membuat UI grafis, masih ada sangat banyak hal yang bisa salah
Meski begitu, ada jalur maju yang praktis, dan itu mengarah ke situasi yang jauh lebih baik daripada keadaan umum sistem masa kini yang penuh kerentanan
[1]: https://people.mpi-sws.org/~dreyer/papers/rustbelt/paper.pdf
[2]: https://plv.mpi-sws.org/rustbelt/stacked-borrows/
[3]: https://www.ralfj.de/blog/2023/06/02/tree-borrows.html
[4]: https://github.com/rust-lang/rust/issues?q=is%3Aissue+is%3Ao...
[5]: https://github.com/Speykious/cve-rs
https://github.com/search?q=repo%3Arust-lang%2Frust+unsafe+l...
Saya tidak berpikir Coq dengan cara yang disajikan di sini bisa memverifikasi semua pemanggilan unsafe
Bisakah dibandingkan bagaimana pendekatan ini berbeda dari Aeneas atau RustHornBelt? Bagaimana pointer dan mutable borrow ditangani?
Dibandingkan dengan Aeneas, tujuannya sangat mirip. Sebab keduanya sama-sama berupaya memverifikasi program Rust dengan interactive theorem prover. Namun di coq-of-rust, kami menulis secara manual versi kode yang murni fungsional sebagai target pembuktian, atau karena pekerjaannya repetitif, menulisnya dengan bantuan GitHub Copilot, lalu membuktikan bahwa versi itu ekuivalen dengan hasil terjemahan otomatis. Aeneas bertujuan menghasilkan versi fungsional tersebut secara langsung
Semua pointer diperlakukan seolah-olah merupakan pointer mutable, yaitu tipe
*. Informasi dari borrow checker Rust tidak digunakan; ini membuat terjemahannya lebih sederhana, tetapi biayanya dibayar pada saat pembuktianUntuk menalar tentang pointer dalam pembuktian, kami memungkinkan pengguna menyediakan allocator khusus yang dapat dirancang sesuai bagaimana memori akan digunakan. Misalnya, jika program menggunakan tiga variabel mutable global, memori bisa dibuat sebagai record dengan tiga entri. Entri-entri ini awalnya bernilai
Noneuntuk menunjukkan keadaan belum dialokasikanKami belum tahu sejauh mana teknik ini dapat diskalakan, tetapi setidaknya untuk saat ini kami bisa menghindari penalaran dengan separation logic. Kami berharap sebagian besar program yang ingin diverifikasi, terutama di sisi aplikasi, memiliki disiplin memori yang relatif “sederhana”
Apakah menulis spesifikasi verifikasi formal mirip dengan menggunakan property-based testing yang lebih kompleks? Bahkan menulis property-based test juga cukup sulit dan memakan waktu begitu melewati program yang tidak rumit
Saat melakukan verifikasi formal pada level yang sama, tampilannya bisa cukup mirip. Namun alat verifikasi formal dapat masuk lebih dalam. Misalnya, alat itu bisa menjawab pertanyaan seperti “ada invariant untuk loop ini; bisakah dibuktikan bahwa invariant tersebut benar-benar dipertahankan pada setiap iterasi?” atau “bisakah dibuktikan bahwa selama komputasi ini tidak pernah terjadi underflow/overflow pada perhitungan antara mana pun?”
Yang pertama juga bisa dilakukan dengan property-based testing jika inti loop dipisahkan menjadi prosedur tersendiri lalu dijalankan pada banyak state antara untuk menguji properti invariant. Yang kedua jauh lebih sulit kecuali program dipecah secara ekstrem sampai bisa dijalankan terpisah per baris
Saya bahkan tidak tahu bahwa hal seperti ini mungkin dilakukan
Saya penasaran apakah upaya semacam ini bisa mempercepat adopsi Rust pada bagian-bagian inti untuk masuk ke kernel
Bisakah seseorang menjelaskan konsep “verifikasi” dengan sangat sederhana? Saya penasaran mengapa ada satu bahasa utuh seperti Coq hanya untuk tujuan ini, dan apa makna praktisnya bagi masyarakat luas