- Rust प्रोग्रामों को Coq में ले जाने वाला
coq-of-rustअब standard library के core·alloc crates तक संभालने लगा है, जिससे primitive functions के लिए अलग-अलग Coq definitions हाथ से लिखने का बोझ कम हुआ है - ये दोनों crates unsafe और उन्नत Rust कोड से भरे बड़े codebase हैं, इसलिए स्वचालित अनुवाद के नतीजों को compile और verify किए जा सकने वाले units में संभालना मुख्य चुनौती बन गया
- आउटपुट को input Rust file के आधार पर बाँटने के बाद
alloc54 files और 171,783 lines, तथाcore190 files और 592,065 lines का हो गया, जिससे parallel compilation और debugging आसान हुई implblocks में module name collisions कोwhereclause की जानकारी शामिल करके कम किया गया, लेकिन अभी भी कुल files में से 4% ऐसे हैं जो Coq में compile नहीं होतेOption::unwrap_or_defaultका उदाहरण इस तरह इस्तेमाल किया गया कि स्वचालित अनुवादित definition और एक सरल functional definition की समानता सिद्ध की जाए; इसके लिए automation पर भरोसे और proof समय की जाँच, दोनों की ज़रूरत है
Rust standard library में primitive handling
- Formal Land, Rust प्रोग्रामों को Coq formal proof system में अनुवाद करने वाला
coq-of-rustविकसित कर रहा है - पहले Rust standard library के primitive components को संभालने के लिए हर function के व्यवहार को दर्शाने वाली Coq definition अलग से बनानी पड़ती थी
- उदाहरण:
Option::unwrap_or_default - हाथ से लिखी definitions दोहराव वाली होती थीं और उनमें गलती आना आसान था
- उदाहरण:
- इस बोझ को घटाने के लिए Rust के
coreऔरalloccrates कोcoq-of-rustसे अनुवादित किया गया - अनुवाद के नतीजे इन paths पर देखे जा सकते हैं
शुरुआती translation run के नतीजे
coq-of-rustकोallocऔरcoreपर पहली बार चलाने पर, हर crate के पूरे हिस्से के लिए लाखों के करीब lines वाले Coq files 2 बने- इससे यह तो साबित हुआ कि tool बड़े codebase पर चल सकता है, लेकिन बना हुआ Coq code तुरंत compile नहीं हुआ
- errors कम थे, लेकिन लगभग हर कुछ हज़ार lines पर एक error आ जाती थी
clocके अनुसार input Rust code का आकार यह थाalloc: Rust code 26,299 linescore: Rust code 54,192 lines
- translation के दौरान macro expansion होता है, इसलिए वास्तविक translation target मूल line count से बड़ा होता है
बने हुए Coq code का विभाजन
- सबसे बड़ा बदलाव यह था कि
coq-of-rustके output को हर input Rust file के लिए एक Coq file में बाँटा गया - यह विभाजन इसलिए संभव हुआ क्योंकि translation definition order के प्रति insensitive और context-free है
- Rust files के बीच आम तौर पर circular dependencies होती हैं, जिन्हें Coq अनुमति नहीं देता, लेकिन इस translation approach में file-level separation संभव रहा
- विभाजन के बाद output का आकार यह बना
alloc: 54 Coq files, 171,783 lines of Coq codecore: 190 Coq files, 592,065 lines of Coq code
- files बाँटने से generated code को देखना और maintain करना आसान हुआ
- compilation को parallelize करना आसान हुआ
- एक-एक file पर ध्यान देकर debugging की जा सकती है
- जो files compile नहीं होतीं उन्हें अलग रखना आसान है
- single-file diff को ट्रैक करना आसान है
module name collision fixes और बची हुई files
- कुछ bugs
implblocks में module name collisions से आए थे - समाधान यह था कि module name में और जानकारी जोड़कर uniqueness बढ़ाई जाए
- पहले छूटी हुई
whereclause की जानकारी जोड़ी गई - उदाहरण के लिए,
Mapping<K, V>के लिएDefaulttrait implementation में यह शर्त किKऔरVदोनोंDefaultimplement करें, module name में दिखाई जाती है
- पहले छूटी हुई
- अभी Coq में compile न होने वाली files ये हैं
alloc/boxed.vcore/any.vcore/array/mod.vcore/cmp/bytewise.vcore/error.vcore/escape.vcore/iter/adapters/flatten.vcore/net/ip_addr.v
- यह कुल files का 4% है
- compile होने वाली files के अंदर भी कुछ Rust constructs अभी axiomatized हैं, इसलिए सिर्फ इस अनुपात से पूरे unsupported area का आकलन करना कठिन है
Option::unwrap_or_default translation example
- Rust का
Option::unwrap_or_default,Some(x)होने परxलौटाता है औरNoneहोने परT::default()को call करता है coq-of-rustइसे monadic form की Coq definition में अनुवादित करता है- input arguments और types को match करता है
Somebranch में tuple field लेकर copy करता हैNonebranch मेंcore::default::Defaulttrait कीdefaultmethod को call करता है
- वास्तविक verification में auto-generated definition की जगह एक सरल functional definition इस्तेमाल की जाती है
Noneहोने परcore.simulations.default.Default.defaultलौटाया जाता हैSome xहोने परxलौटाया जाता है
- auto-generated definition और सरल definition की समानता का proof
CoqOfRust/core/proofs/option.vमें है - अगर मूल Rust code बदलता है, तो यह proof उस बदलाव को पकड़ लेता है
corelibrary का translation अपने-आप हुआ था, इसलिए हाथ से लिखी definitions की तुलना में generated definitions पर अधिक भरोसा किया जा सकता है- फिर भी
coq-of-rustमें गलती या अपूर्णता हो सकती है, इसलिए proof के समय code की वैधता जाँचना ज़रूरी है
बाकी चुनौतियाँ
- Rust प्रोग्राम verification में standard library formalization पर भरोसा बढ़ा है
- अगला लक्ष्य अब भी उबाऊ proof process को सरल बनाना है
- खासकर यह दिखाने के लिए कि simulation मूल Rust code के बराबर है, ये काम ज़रूरी हैं
- name resolution
- high-level types का परिचय
- side effects हटाना
- इन चरणों को अलग-अलग संभालना आगे के सुधार की दिशा है
1 टिप्पणियां
Hacker News की राय
वाकई प्रभावशाली
ऐसा automatic translation भरोसे के लक्ष्य को tool की ओर शिफ्ट कर देता है। coq-of-rust खुद Coq में नहीं, बल्कि Rust में लिखा गया है, इसलिए इसकी recursive संरचना काफी हैरान करने वाली है। लेकिन David A. Wheeler के “Countering Trusting Trust through Diverse Double-Compiling” (2009) [0] की तरह, दूसरे compiler के जरिए Ken Thompson के Trusting Trusting attack को bypass करने के तरीके में CompCert-स्टाइल approach मिलाई जाए, तो correctness proof संभव लगता है
verify करने के लिए coq-of-rust से coq-of-rust translator को Rust से Coq में बदलना होगा। और चूंकि वह translation Rust से की गई है, भले उस पर भरोसा न करें, Coq के अंदर वांछित correctness properties—खासकर यह कि Rust program को Coq में translate करते समय semantics preserve होते हैं—prove कर देना पर्याप्त होगा
paper की तरह generated definitions की बजाय अधिक functional definitions पर proof करना आसान होने की संभावना है, इसलिए standard library में जैसे definitions के बीच equivalence prove किया जाता है, वैसी प्रक्रिया अपनाई जा सकती है। अगर मौजूदा coq-of-rust translator, खासकर lib/ [1], Rust की 6,350 lines का है, तो पूरा translator Coq में लिखना और generated version के साथ उसकी equivalence prove करना भी व्यावहारिक लगता है
इसके बाद proven Coq-version coq-of-rust translator को Rust में लिखे coq-of-rust source पर चलाएँ, तो output होने वाली Coq definitions शुरू में इस्तेमाल किए गए Rust-version coq-of-rust translator के output से match करनी चाहिए
वैसे, ऐसे काम को industry funding मिलते देखना अच्छा है। मैं cryptocurrency को लेकर थोड़ा cynical हूं, लेकिन यह सच है कि उस तरफ की correctness requirements Rust, Coq और परिचित master's students को support करने जैसे रुचि क्षेत्रों में सुधार को आगे बढ़ा रही हैं
[0]: https://dwheeler.com/trusting-trust/wheelerd-trust.pdf
[1]: https://github.com/formal-land/coq-of-rust/tree/main/lib
हालांकि code खुद छोटा है, फिर भी input Rust code को parse और type-check करने के लिए Rust compiler पर निर्भर करता है। इसलिए उस हिस्से को भी verify करना होगा, या कम से कम बिना proof के formal specification देनी होगी। rustc का API काफी बड़ा और unstable है, यह एक बाधा है, लेकिन फिर भी भरोसा बढ़ाने का एक तरीका हो सकता है
बेशक output या converter को verify करने की समस्या आती थी, लेकिन resulting C code verification के लिहाज से काफी readable होता था और उसका style भी constrained था, और tools पर trust भी ज्यादा था। SPARK static analysis पूरी verification/validation प्रक्रिया का हिस्सा थी, और tests व दूसरी activities अतिरिक्त trust layers देती थीं। कुल मिलाकर यह approach काफी अच्छी तरह काम करती थी
वे वह पैसा computer science research पर सिर्फ इसलिए नहीं खर्च करते कि इससे उन्हें फायदा होता है, बल्कि इसलिए भी कि यह उनके hobby से जुड़ी philanthropy है
दूसरा approach proof-carrying code है। इसमें Rust compiler executable output के साथ एक Coq proof भी emit करता है कि output input source code की semantics से मेल खाता है
बेशक Rust के subset compiler को, उदाहरण के लिए borrow checker या optimization के बिना, किसी specific architecture के machine code में लिखकर, वहां से सब कुछ bootstrap भी किया जा सकता है
Coq जैसे semi-automatic deductive proof system से शुरू से अंत तक verified programs का आकार छोटा होता है। Libraries आसान हो सकती हैं क्योंकि code के हिस्से आपस में कम interact करते हैं, लेकिन general programs में ऐसा नहीं होता
असल में इस तरह verify किए जा सकने वाले programs के size में बढ़ोतरी, पूरे programs के average size में बढ़ोतरी से धीमी रही है। Sound software verification, यानी specification से 100% match दिखाना, core algorithms की correctness proof जैसी जगहों पर जरूर उपयोगी है, लेकिन यह अच्छी तरह scale नहीं करता
इसलिए research कुछ हद तक unsound methods की ओर गई है। 100% guarantee की cost, 99.9999% guarantee की cost से 10 गुना हो सकती है, और probability का वह फर्क software से बाहर की failure probability से भी छोटा हो सकता है। Physical systems के बारे में तो शुरू से ही यह prove नहीं किया जा सकता कि वे specification से match करते हैं, और specification खुद reality से पर्याप्त रूप से match न करे, इसकी भी संभावना होती है
सिर्फ unsafe code को correct prove करना, सभी Rust code को prove करने की तुलना में बहुत कम effort में संभव है। यह Rust की safety guarantees पर बात करते समय अक्सर उठने वाली “unsafe code का क्या?” वाली criticism का जवाब है। Rust type system अकेले जिन gaps को संभालने के लिए पर्याप्त powerful नहीं है, उन्हें Coq जैसे stronger system से भरा जा सकता है
इस तरह की कोशिशों में एक बात ठीक से समझ नहीं आती। अगर code को Coq में manually या semi-manually बदलना पड़े, तो क्या उस प्रक्रिया में गलती होने की संभावना formal verification से मिलने वाले फ़ायदे से कहीं ज़्यादा नहीं होगी?
यानी हमें कैसे पता चलेगा कि हमने जो prove किया है वह अब भी original code पर लागू है?
formal verification पर अक्सर ऐसे चर्चा होती है जैसे यह bug की संभावना को पूरी तरह खत्म कर देती हो, लेकिन असल में यह bug की संभावना को काफी कम करने के ज्यादा करीब है। फिर भी Rust और Coq के बीच automatic translation उन चीज़ों की complexity को बहुत घटा देता है जिन पर भरोसा करना पड़ता है, इसलिए इसे manual translation से बेहतर माना जाना चाहिए।
कई मामलों में translation का bug बस proof को असंभव बना देता है। तब कोई यह जांचते हुए कि proof pass क्यों नहीं हो रहा, translation bug ढूंढ लेता है।
असली समस्या तब है जब translation bug original code के bug को बिल्कुल ठीक-ठीक cancel कर दे। अगर कोई systematic risk न हो, तो दो bugs का इस तरह एक-दूसरे को मिटा देना काफी कम संभावित है।
इच्छुक readers के लिए: मैंने यह blog post इसलिए submit की क्योंकि उसी blog की दूसरी posts की तुलना में इसमें cryptocurrency से जुड़ी बात कम directly है, लेकिन वहां technically और भी interesting posts हैं।
खासकर हाल की दो posts इसी approach को Rust के बजाय Python पर apply करने के बारे में हैं।
मुझे पहले की एक lecture [1] याद है जिसमें formally verified C compiler में fuzzer ने bug ढूंढा था। वजह यह थी कि formal verification में frontend और backend शामिल नहीं थे।
मुझे पता है कि Coq खुद या translation पर कितना भरोसा किया जा सकता है, यह सवाल भी उठता है, और यह भी सवाल है कि Rust updates के साथ sync कैसे होगा, लेकिन perfect formal verification भी शुरू से अंत तक 100% correctness का मतलब नहीं होती।
[1] https://youtu.be/Ux0YnVEaI6A
मैं formal verification expert बिल्कुल नहीं हूं, लेकिन अगर Rust की basic library formally verified हो और unsafe code का इस्तेमाल न करे, तो क्या formally verified library इस्तेमाल करने वाले सभी Rust programs memory handling के लिहाज से practically formal verification-level quality के हो जाएंगे?
Rust की अपनी “safe” definition है, जिसे memory safety का subset माना जा सकता है। पूरी तरह safe Rust code में भी data races, deadlocks, memory exhaustion वगैरह संभव हैं, logical errors की तो बात ही अलग है।
पहला, unsafe Rust की semantics को formalize करना होगा। Ralf Jung का pioneering RustBelt[1] काम बड़ा कदम था, लेकिन अभी complete नहीं है। खासकर pointer provenance एक मुश्किल factor साबित हो रहा है।
दूसरा, इसके हिस्से के रूप में borrow checker का formal model चाहिए। Stacked borrows[2] एक अच्छा प्रयास था, लेकिन उसमें flaws हैं, और Tree borrows[3] शायद इसे ठीक कर सकता है, हालांकि और भी refined कुछ आने की संभावना है।
तीसरा, formal memory model चाहिए। यह मुख्यतः atomic operations और synchronization के behavior से जुड़ा है, इसलिए Arc जैसे standard library components के लिए बहुत अहम है। यह व्यापक रूप से माना जाता है कि Rust का memory model C++ जैसा होना चाहिए और उसके साथ interoperable होना चाहिए, लेकिन “out of thin air” problem और seqlock जैसी missing capabilities बाकी हैं। Linux kernel के अभी भी अपना model इस्तेमाल करने की एक वजह यही है।
चौथा, यह proof चाहिए कि safety guarantees अच्छी तरह compose होती हैं। खास तौर पर, unsafe Rust में लिखे sound code और safe Rust code को compose करने पर भी safety guarantees बनी रहनी चाहिए। अब तक अच्छे results हैं, लेकिन पूरे system के लिए इसे prove करना होगा।
पांचवां, ऐसे proofs सभी code पर लागू हों, इसके लिए Rust में बचे सभी soundness bugs[1] बंद करने होंगे। इनमें से कई issues theoretical हैं या adversarial code[5] में ही practically important होते हैं, इसलिए progress slow है।
यह सब हो जाने के बाद भी guarantee अभी भी partial ही होगी। system से जुड़ने वाली surface का बहुत बड़ा हिस्सा unsafe code पर आधारित है। अगर आप सिर्फ pure computation कर रहे हों तो अलग बात है, लेकिन उदाहरण के लिए graphics UI बना रहे हों, तो अब भी बहुत कुछ गलत हो सकता है।
फिर भी practical आगे बढ़ने का रास्ता है, और वह आज की vulnerability-भरी systems की सामान्य स्थिति से कहीं बेहतर स्थिति तक ले जाता है।
[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...
मुझे नहीं लगता कि यहां सुझाए गए तरीके वाला Coq सभी unsafe calls को verify कर सकता है।
क्या इस approach की तुलना Aeneas या RustHornBelt से की जा सकती है? pointers और mutable borrows को कैसे handle किया जाता है?
Aeneas से तुलना करें तो लक्ष्य काफी मिलता-जुलता है, क्योंकि दोनों interactive theorem prover में Rust programs को verify करना चाहते हैं। हालांकि coq-of-rust में proof का target बनने वाले code का pure functional version हाथ से लिखा जाता है, या दोहराए जाने वाले काम के लिए GitHub Copilot की मदद से लिखा जाता है, और फिर साबित किया जाता है कि यह automatically translated result के equivalent है। Aeneas का लक्ष्य functional version को सीधे generate करना है
pointers को सभी mutable pointers, यानी
*type की तरह treat किया जाता है। Rust के borrow checker की जानकारी इस्तेमाल नहीं की जाती; इससे translation सरल हो जाती है, लेकिन इसकी कीमत proof के समय चुकानी पड़ती हैproofs में pointers के बारे में reasoning करने के लिए user को custom allocator देने दिया जाता है, जिसे memory कैसे इस्तेमाल होगी उसके हिसाब से design किया जा सकता है। उदाहरण के लिए, अगर program तीन global mutable variables इस्तेमाल करता है, तो memory को तीन items वाले record के रूप में रखा जा सकता है। ये items शुरुआत में
Noneहोते हैं, ताकि यह दिखाया जा सके कि वे अभी allocated नहीं हैंयह technique कितनी scale करेगी, यह अभी नहीं पता, लेकिन कम से कम फिलहाल इससे separation logic reasoning से बचा जा सकता है। हम उम्मीद करते हैं कि verify किए जाने वाले ज्यादातर programs, खासकर application side वाले, अपेक्षाकृत “simple” memory discipline रखेंगे
क्या formal verification specification लिखना ज्यादा complex property-based testing इस्तेमाल करने जैसा है? साधारण programs से आगे बढ़ने पर property-based tests लिखना भी काफी मुश्किल और समय लेने वाला हो जाता है
उसी level पर formal verification करने पर यह काफी वैसा ही दिख सकता है। लेकिन formal verification tools और गहराई तक जा सकते हैं। उदाहरण के लिए, वे computation के अंदर system state को लेकर ऐसे सवालों के जवाब दे सकते हैं: “यह loop invariant है, क्या साबित किया जा सकता है कि यह हर iteration में सच में बना रहता है?” या “क्या साबित किया जा सकता है कि इस computation के दौरान किसी भी intermediate calculation में underflow/overflow कभी नहीं होता?”
पहला काम property-based testing से भी संभव है, अगर loop के core को अलग procedure में निकालकर कई intermediate states के साथ चलाया जाए और invariant property test की जाए। दूसरा कहीं ज्यादा कठिन है, जब तक कि program को इतना ज्यादा न तोड़ा जाए कि उसे line-by-line अलग से execute किया जा सके
मुझे पता ही नहीं था कि ऐसा भी संभव है
सोच रहा हूं कि क्या ऐसी कोशिशें kernel adoption के core हिस्सों में Rust adoption को तेज कर सकती हैं
क्या कोई “verification” concept को बहुत आसान तरीके से समझा सकता है? जानना चाहता हूं कि सिर्फ इसी उद्देश्य के लिए Coq जैसी पूरी language क्यों मौजूद है, और व्यापक समाज के लिए इसका practically क्या मतलब है