- Rust programlarını Coq’a aktaran
coq-of-rust, standart kütüphanenin core ve alloc crate’lerini de ele almaya başlayarak primitive fonksiyonlar için Coq tanımlarını elle yazma yükünü azaltıyor - Bu iki crate, unsafe ve ileri seviye Rust kodu içeren büyük kod tabanları olduğundan, otomatik çeviri çıktısını derlenebilir ve doğrulanabilir birimler olarak ele almak temel zorluk haline geliyor
- Çıktılar giriş Rust dosyası bazında bölününce
alloc54 dosya ve 171.783 satıra,coreise 190 dosya ve 592.065 satıra ulaştı; paralel derleme ve hata ayıklama kolaylaştı implbloklarındaki modül adı çakışmaları,wherekoşulu bilgileri dahil edilerek azaltıldı; ancak şu anda Coq’ta derlenmeyen dosyalar toplamın %4’ü olarak kalıyorOption::unwrap_or_defaultörneği, otomatik çeviri tanımı ile basit fonksiyonel tanımın eşdeğerliğini kanıtlayarak kullanma yaklaşımını gösteriyor; bu da hem otomasyona güven hem de kanıt anında kontrol gerektiriyor
Rust standart kütüphanesindeki primitive’lerin ele alınması
- Formal Land, Rust programlarını Coq biçimsel kanıt sistemine çeviren
coq-of-rustüzerinde çalışıyor - Önceden, Rust standart kütüphanesinin primitive bileşenlerini ele almak için her fonksiyonun davranışını gösteren ayrı Coq tanımları oluşturmak gerekiyordu
- Örnek:
Option::unwrap_or_default - Elle yazılan tanımlar tekrarlıydı ve hata girmesine açıktı
- Örnek:
- Bu yükü azaltmak için Rust’ın
corevealloccrate’lericoq-of-rustile çevrildi - Çeviri sonuçları aşağıdaki yollardan görülebilir
İlk çeviri çalıştırmasının sonuçları
coq-of-rust,allocvecoreüzerinde ilk kez çalıştırıldığında, her bir crate’in tamamına karşılık gelen yüz binlerce satırlık iki Coq dosyası üretildi- Aracın büyük kod tabanlarında da çalışabildiği doğrulandı, ancak üretilen Coq kodu doğrudan derlenemedi
- Hatalar seyrek görülüyordu; birkaç bin satırda yaklaşık bir hata düzeyindeydi
clocölçümüne göre giriş Rust kodunun boyutu şöyleydialloc: 26.299 satır Rust koducore: 54.192 satır Rust kodu
- Çeviri sürecinde makro genişletme gerçekleştiği için gerçek çeviri hedefi özgün satır sayısından daha büyüktür
Üretilen Coq kodunun bölünmesi
- En büyük değişiklik,
coq-of-rustçıktısını her giriş Rust dosyası için bir Coq dosyası olacak şekilde bölmekti - Bu bölme, çevirinin tanım sırasına duyarsız ve context-free olması sayesinde mümkün oldu
- Rust dosyaları arasında genellikle döngüsel bağımlılıklar bulunur ve Coq buna izin vermez; ancak bu çeviri yönteminde dosya bazlı ayırma mümkündür
- Bölmeden sonra çıktı boyutu şöyle oldu
alloc: 54 Coq dosyası, 171.783 satır Coq koducore: 190 Coq dosyası, 592.065 satır Coq kodu
- Dosyaların bölünmesiyle üretilen kodda gezinmek ve bakımı kolaylaştı
- Derlemeyi paralelleştirmek daha kolay
- Tek tek dosyalara odaklanarak hata ayıklanabilir
- Derlenmeyen dosyaları hariç tutmak daha kolay
- Tek dosyalık diff’leri izlemek daha kolay hale geldi
Modül adı çakışmalarının düzeltilmesi ve kalan dosyalar
- Bazı hatalar,
implbloklarındaki modül adı çakışmalarından kaynaklandı - Çözüm, modül adına daha fazla bilgi ekleyerek benzersizliği artırmaktı
- Önceden eksik olan
wherekoşulu bilgileri dahil edildi - Örneğin
Mapping<K, V>içinDefaulttrait implementasyonunda, hemKhem deViçinDefaultimplementasyonu gerektiği koşulu modül adına yansıtılır
- Önceden eksik olan
- Şu anda Coq’ta derlenmeyen dosyalar şunlar
alloc/boxed.vcore/any.vcore/array/mod.vcore/cmp/bytewise.vcore/error.vcore/escape.vcore/iter/adapters/flatten.vcore/net/ip_addr.v
- Bu, tüm dosyaların %4’üne karşılık geliyor
- Derlenen dosyaların içinde de henüz ele alınmamış Rust bileşenleri aksiyomlaştırılmış durumda; bu nedenle yalnızca bu oranla tüm desteklenmeyen kapsamı değerlendirmek zor
Option::unwrap_or_default çeviri örneği
- Rust’taki
Option::unwrap_or_default, değerSome(x)isexdöndürür,NoneiseT::default()çağırır coq-of-rustbunu monadic biçimde bir Coq tanımına çevirir- Girdi argümanları ve türleri eşleştirir
Somedalında tuple field’ını alıp kopyalarNonedalındacore::default::Defaulttrait’inindefaultmetodunu çağırır
- Gerçek doğrulamada, otomatik üretilmiş tanım yerine daha basit bir fonksiyonel tanım kullanılır
Noneisecore.simulations.default.Default.defaultdöndürürSome xisexdöndürür
- Otomatik üretilmiş tanım ile basit tanımın eşdeğer olduğuna dair kanıt
CoqOfRust/core/proofs/option.viçinde bulunur - Özgün Rust kodu değişirse bu kanıt aracılığıyla değişiklik yakalanır
corekütüphanesinin çevirisi otomatik yapıldığı için üretilen tanıma, elle yazılmış tanımdan daha fazla güvenilebilir- Bununla birlikte
coq-of-rustiçinde de hata veya eksiklikler olabileceğinden, kanıt anında kodun geçerli olup olmadığı kontrol edilmelidir
Kalan işler
- Rust programlarının doğrulanmasında standart kütüphane biçimselleştirmesine duyulan güven artıyor
- Bir sonraki hedef hâlâ zahmetli olan kanıt sürecini basitleştirmek
- Özellikle simülasyonun özgün Rust koduyla eşdeğer olduğunu göstermek için şu işler gerekiyor
- İsim çözümleme
- Yüksek seviyeli türlerin devreye alınması
- Yan etkilerin kaldırılması
- Bu aşamaları ayrı ayrı ele almak gelecekteki iyileştirme yönü olacak
1 yorum
Hacker News yorumları
Gerçekten etkileyici
Böyle bir otomatik çeviri, güvenilecek şeyi araca kaydırıyor. coq-of-rust’ın kendisi Coq ile değil Rust ile yazılmış olduğundan özyinelemeli yapı epey şaşırtıcı; ancak David A. Wheeler’ın “Countering Trusting Trust through Diverse Double-Compiling” (2009) [0] çalışmasındaki gibi, Ken Thompson’ın Trusting Trust saldırısını ikinci bir derleyiciyle baypas etme yöntemine CompCert tarzı bir yaklaşım eklenirse doğruluk kanıtı mümkün görünüyor
Doğrulamak için coq-of-rust ile coq-of-rust çeviricisini Rust’tan Coq’a dönüştürmek gerekir; bu çeviri Rust ile yapıldığı için ona güvenmesek bile, Coq içinde istenen doğruluk özellikleri, özellikle Rust programlarını Coq’a çevirirken semantiğin korunduğu kanıtlanabilir
Makaledeki gibi, üretilen tanımlardan daha işlevsel tanımlar üzerinden kanıt yapmak muhtemelen daha kolay olacağından, standart kütüphanede yapıldığı gibi tanımlar arasında eşdeğerliği kanıtlama sürecinden geçilebilir. Mevcut coq-of-rust çeviricisi, özellikle lib/ [1], 6.350 satır Rust ise, çeviricinin tamamını Coq ile yazıp üretilenle eşdeğer olduğunu kanıtlamak da gerçekçi görünüyor
Ardından kanıtlanmış Coq sürümü coq-of-rust çeviricisi, Rust ile yazılmış coq-of-rust kaynağı üzerinde çalıştırıldığında, çıkan Coq tanımları başlangıçta kullanılan Rust sürümü coq-of-rust çeviricisinin çıktısıyla eşleşmeli
Bu arada, böyle işlere endüstri finansmanı gelmesini görmek güzel. Kripto paralara karşı alaycı sayılırım ama o taraftaki doğruluk ihtiyacının Rust, Coq ve tanıdığım yüksek lisans öğrencilerini destekleme gibi ilgi alanlarındaki iyileştirmeleri ittiği de bir gerçek
[0]: https://dwheeler.com/trusting-trust/wheelerd-trust.pdf
[1]: https://github.com/formal-land/coq-of-rust/tree/main/lib
Ancak kodun kendisi kısa olsa da, girdi Rust kodunu ayrıştırmak ve tip denetiminden geçirmek için Rust derleyicisine dayanıyor. Dolayısıyla bu kısmı da doğrulamak ya da en azından kanıtsız da olsa biçimsel olarak belirtimlemek gerekir. rustc’nin API’sinin epey büyük ve kararsız olması bir engel, ama yine de güvenilirliği artırmanın bir yolu olabilir
Elbette çıktıların veya dönüştürücünün doğrulanması sorunu ortaya çıkıyordu, ama sonuç C kodu doğrulama amacıyla oldukça okunabilirdi, stili de sınırlıydı ve araca duyulan güven de yüksekti. SPARK statik analizi tüm doğrulama ve onaylama sürecinin bir parçasıydı; testler ve diğer faaliyetler ek güven katmanları sağlıyordu. Genel olarak oldukça iyi işleyen bir yaklaşımdı
O parayı bilgisayar bilimi araştırmalarına harcamaları yalnızca kendi çıkarlarına olduğu için değil, hobileriyle örtüşen bir hayırseverlik faaliyeti olduğu için de oluyor
Başka bir yaklaşım da kanıt taşıyan kod. Rust derleyicisinin, yürütülebilir çıktıların girdi kaynak kodunun semantiğiyle eşleştiğine dair Coq kanıtını da birlikte üretmesi
Elbette Rust’ın bir alt kümesi için derleyiciyi, örneğin ödünç alma denetleyicisi veya optimizasyon olmadan belirli bir mimarinin makine koduna yazıp, oradan her şeyi bootstrap etmek de mümkün
Coq gibi yarı otomatik tümdengelimli kanıt sistemleriyle baştan sona doğrulanmış programların boyutu küçüktür. Kütüphaneler, kodun birbiriyle etkileşim derecesi daha düşük olduğu için daha kolay olabilir; ancak genel programlar için durum böyle değildir
Gerçekte, bu yöntemle doğrulanabilir programların boyutundaki artış, tüm programların ortalama boyutundaki artıştan daha yavaş kaldı. Sağlam yazılım doğrulaması, yani belirtimle %100 uyumlu olduğunu göstermek, çekirdek algoritmaların doğruluk kanıtı gibi yerlerde kesinlikle faydalıdır ama iyi ölçeklenmez
Bu yüzden araştırmanın sağlam olmayan yöntemlere kaydığı bir yön var. %100 garantinin maliyeti, %99,9999 garantinin 10 katı olabilir ve bu olasılık farkı yazılım dışı arıza olasılığından daha küçük hale gelebilir. Fiziksel sistemlerin zaten belirtimle uyumlu olduğu kanıtlanamaz; belirtimin kendisinin gerçeklikle yeterince örtüşmeme olasılığı da vardır
Yalnızca unsafe kodun doğru olduğunu kanıtlamak, tüm Rust kodunu kanıtlamaktan çok daha az çabayla mümkün. Bu, Rust’ın güvenlik garantileri konuşulurken sıkça dile getirilen “peki unsafe kod ne olacak” eleştirisine bir yanıt oluyor. Rust tip sisteminin tek başına ele almak için yeterince güçlü olmadığı boşluklar, Coq gibi daha güçlü bir sistemle doldurulabilir
Bu tür bir girişimde tam anlayamadığım bir nokta var. Kodu Coq'ya elle ya da yarı elle dönüştürmek gerekiyorsa, bu süreçte hata yapma olasılığı biçimsel doğrulamadan elde edilen faydadan çok daha büyük değil mi?
Başka bir deyişle, kanıtladığımız şeyin hâlâ orijinal kod için geçerli olduğunu nasıl bilebiliriz?
Biçimsel doğrulama çoğu zaman hata olasılığını tamamen ortadan kaldırıyormuş gibi tartışılıyor, ama pratikte daha çok hata olasılığını ciddi ölçüde düşürmeye yakındır. Yine de Rust ile Coq arasında otomatik çeviri, güvenilmesi gereken şeyin karmaşıklığını büyük ölçüde azalttığı için elle çeviriye tercih edilmelidir
Çoğu durumda çevirideki hata, kanıtı basitçe imkânsız hale getirir. O zaman biri kanıtın neden geçmediğini araştırırken çeviri hatasını bulur
Asıl sorun, çeviri hatasının orijinal koddaki hatayı tam olarak dengelemesi durumudur. Sistematik bir risk yoksa iki hatanın bu şekilde birbirini silmesi oldukça düşük olasılıklıdır
İlgilenen okurlara: Bu blog yazısı, aynı blogdaki diğer yazılara göre kripto para ile daha az doğrudan ilgili olduğu için gönderdim; ama orada teknik açıdan daha ilginç pek çok yazı var
Özellikle son iki yazı, aynı yaklaşımı Rust'a değil Python'a uygulamayı ele alıyor
Eskiden biçimsel olarak doğrulanmış bir C derleyicisinde fuzzer'ın hata bulduğuna dair bir ders [1] hatırlıyorum. Bunun nedeni, biçimsel doğrulamanın frontend ve backend'i kapsamamasıydı
Coq'nun kendisine ya da çeviriye ne kadar güvenilebileceği sorusunun ortaya çıktığını da biliyorum; Rust güncellemeleriyle nasıl senkronize olacağı da soru işareti, ama kusursuz bir biçimsel doğrulama bile baştan sona %100 doğruluk anlamına gelmez
[1] https://youtu.be/Ux0YnVEaI6A
Biçimsel doğrulama uzmanı hiç değilim ama Rust'ın temel kütüphanesi biçimsel olarak doğrulanır ve unsafe kod kullanmazsa, biçimsel olarak doğrulanmış kütüphaneyi kullanan tüm Rust programları bellek işleme açısından fiilen biçimsel doğrulama düzeyinde kaliteye sahip mi olur?
Rust'ın kendi “safe” tanımı vardır ve bu, bellek güvenliğinin bir alt kümesi olarak görülebilir. Tamamen güvenli Rust kodunda bile veri yarışları, deadlock'lar, belleğin tükenmesi vb. mümkündür; mantık hatalarından bahsetmiyorum bile
Birincisi, unsafe Rust'ın semantiğini biçimselleştirmek gerekir. Ralf Jung'un öncü RustBelt[1] çalışması büyük bir ilerlemeydi ama henüz tamamlanmış değil. Özellikle işaretçi kökeni zorlu bir unsur olarak ortaya çıkıyor
İkincisi, bunun bir parçası olarak borrow checker'ın biçimsel modeline ihtiyaç var. Stacked borrows[2] iyi bir denemeydi ama kusurları var; Tree borrows[3] bunu düzeltebilir, ancak daha incelikli bir şeyin ortaya çıkması da mümkün
Üçüncüsü, biçimsel bir bellek modeline ihtiyaç var. Bu esas olarak atomik işlemlerin ve senkronizasyonun davranışıyla ilgilidir; dolayısıyla Arc gibi standart kütüphane bileşenleri için çok önemlidir. Rust'ın bellek modelinin C++'a benzemesi ve onunla birlikte çalışması gerektiği geniş ölçüde kabul görüyor, ancak “out of thin air” sorunu ve seqlock gibi eksik özellikler hâlâ duruyor. Linux kernel'ının hâlâ kendi modelini kullanmasının nedenlerinden biri de bu
Dördüncüsü, güvenlik garantilerinin iyi biçimde bileşime girdiğinin kanıtlanması gerekir. Özellikle unsafe Rust ile yazılmış sound kod ile safe Rust kodu birleştirildiğinde de güvenlik garantisi korunmalıdır. Şimdiye kadar iyi sonuçlar var, ancak tüm sistem için kanıtlanması gerekir
Beşincisi, böyle bir kanıtın tüm kodlar için geçerli olabilmesi için Rust'ta kalan tüm soundness bug'larının[1] kapatılması gerekir. Bu sorunların çoğu teoriktir ya da ancak saldırgan kodda[5] pratikte önemli hale gelir, bu yüzden ilerleme yavaştır
Bütün bunlar tamamlandığında bile hâlâ yalnızca kısmi bir garanti olur. Sistemle temas eden yüzeyin muazzam bir kısmı unsafe koda dayanır. Yalnızca saf hesaplama yapıyorsanız belki, ama örneğin grafik arayüz geliştiriyorsanız hâlâ ters gidebilecek çok şey vardır
Yine de pratik bir ilerleme yolu var ve bu, bugünün güvenlik açıklarıyla dolu sistemlerinin genel durumundan çok daha iyi bir noktaya götürür
[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...
Burada önerilen türden Coq'nun tüm unsafe çağrılarını doğrulayabileceğini sanmıyorum
Bu yaklaşımın Aeneas veya RustHornBelt ile nasıl farklılaştığını karşılaştırabilir misiniz? İşaretçiler ve değiştirilebilir ödünç almalar nasıl ele alınıyor?
Aeneas ile karşılaştırınca hedefler çok benzer. Çünkü ikisi de etkileşimli teorem kanıtlayıcılarla Rust programlarını doğrulamayı amaçlıyor. Ancak coq-of-rust’ta kanıtın hedefi olan saf işlevsel kod sürümünü elle yazıyoruz ya da tekrar eden bir iş olduğu için GitHub Copilot’tan yardım alarak yazıyoruz ve bunun otomatik çeviri sonucuyla eşdeğer olduğunu kanıtlıyoruz. Aeneas ise işlevsel sürümü doğrudan üretmeyi hedefliyor.
İşaretçilerin hepsini değiştirilebilir işaretçi, yani
*türündeymiş gibi ele alıyoruz. Rust’ın ödünç alma denetleyicisi bilgisini kullanmıyoruz; bu çeviriyi basitleştiriyor ama bedelini kanıt aşamasında ödüyoruz.Kanıtta işaretçiler üzerinde akıl yürütmek için, kullanıcının belleğin nasıl kullanılacağına göre tasarlayabileceği özel bir ayırıcı sağlamasına izin veriyoruz. Örneğin program üç global değiştirilebilir değişken kullanıyorsa, belleği üç öğeli bir kayıt olarak ele alabiliriz. Bu öğeler, henüz ayrılmamış durumu göstermek için başlangıçta
Noneolur.Bu tekniğin ne kadar ölçekleneceğini henüz bilmiyoruz, ama en azından şimdilik ayrık mantıkla akıl yürütmekten kaçınabiliyoruz. Doğrulamak istediğimiz çoğu programın, özellikle de uygulama tarafındakilerin, nispeten “basit” bir bellek disiplinine sahip olmasını bekliyoruz.
Biçimsel doğrulama belirtimleri yazmak, daha karmaşık özellik tabanlı test kullanmaya benziyor mu? Çok basit olmayan programların ötesine geçince özellik tabanlı test yazmak da oldukça zor ve zaman alıcı oluyor.
Aynı düzeyde biçimsel doğrulama yaparken de oldukça benzer görünebilir. Ancak biçimsel doğrulama araçları daha derine inebilir. Örneğin hesaplamanın içindeki sistem durumu hakkında “bu döngü değişmezi var; bunun her iterasyonda gerçekten korunduğunu kanıtlayabilir miyiz?” veya “bu hesaplama sırasında hiçbir ara hesaplamada asla underflow/overflow oluşmadığını kanıtlayabilir miyiz?” gibi sorulara yanıt verebilir.
İlki, döngünün çekirdeğini ayrı bir prosedüre çıkarıp çok sayıda ara durumla çalıştırarak değişmezlik özelliğini test ederseniz özellik tabanlı testle de mümkün olabilir. İkincisi ise programı satır satır ayrı çalıştırılabilir hale getirecek kadar aşırı parçalamadığınız sürece çok daha zordur.
Böyle bir şeyin mümkün olduğunu bile bilmiyordum.
Bu tür girişimlerin, çekirdeğe alma sürecinin kritik kısımlarında Rust benimsenmesini hızlandırıp hızlandıramayacağını merak ediyorum.
Biri “doğrulama” kavramını çok basitçe açıklayabilir mi? Neden yalnızca bu amaç için Coq gibi başlı başına bir dilin var olduğunu ve bunun daha geniş toplum açısından ne gibi pratik anlamı olduğunu merak ediyorum.