coq-of-rustซึ่งใช้ย้ายโปรแกรม Rust ไปยัง Coq เริ่มรองรับ cratecoreและallocของไลบรารีมาตรฐานแล้ว ช่วยลดภาระที่ก่อนหน้านี้ต้องเขียน นิยาม Coq ของฟังก์ชัน primitive ทีละตัวด้วยมือ- เนื่องจากทั้งสอง crate เป็นโค้ดเบสขนาดใหญ่ที่มีทั้ง unsafe และโค้ด Rust ขั้นสูงจำนวนมาก โจทย์สำคัญจึงอยู่ที่การจัดการผลลัพธ์จากการแปลให้อยู่ในหน่วยที่สามารถคอมไพล์และตรวจพิสูจน์ได้
- เมื่อแยกผลลัพธ์ออกตามไฟล์ Rust ต้นทาง
allocมี 54 ไฟล์ 171,783 บรรทัด และcoreมี 190 ไฟล์ 592,065 บรรทัด ทำให้คอมไพล์แบบขนานและดีบักได้ง่ายขึ้น - ปัญหา ชื่อโมดูลชนกัน ในบล็อก
implถูกบรรเทาด้วยการใส่ข้อมูลจากเงื่อนไขwhereเพิ่มเข้าไป แต่ตอนนี้ยังมีไฟล์ที่คอมไพล์บน Coq ไม่ผ่านอยู่ 4% ของทั้งหมด - ตัวอย่าง
Option::unwrap_or_defaultแสดงวิธีใช้งานโดยพิสูจน์ว่านิยามที่แปลอัตโนมัติกับนิยามเชิงฟังก์ชันแบบง่ายมีความสมมูลกัน ซึ่งต้องอาศัยทั้งความเชื่อมั่นต่อระบบอัตโนมัติและการตรวจสอบในขั้นพิสูจน์
การจัดการ primitive ของไลบรารีมาตรฐาน Rust
- Formal Land กำลังพัฒนา
coq-of-rustเพื่อแปลโปรแกรม Rust ไปยังระบบพิสูจน์เชิงรูปนัย Coq - ก่อนหน้านี้ เพื่อจัดการองค์ประกอบ primitive ของไลบรารีมาตรฐาน Rust จำเป็นต้องสร้าง นิยาม Coq แยกต่างหากเพื่ออธิบายพฤติกรรมของแต่ละฟังก์ชัน
- ตัวอย่าง:
Option::unwrap_or_default - การนิยามด้วยมือเป็นงานที่ซ้ำซากและแทรกข้อผิดพลาดได้ง่าย
- ตัวอย่าง:
- เพื่อลดภาระนี้ จึงมีการแปล crate
coreและallocของ Rust ด้วยcoq-of-rust - สามารถดูผลลัพธ์การแปลได้ที่
ผลลัพธ์จากการรันแปลครั้งแรก
- เมื่อนำ
coq-of-rustไปรันกับallocและcoreในครั้งแรก ได้ไฟล์ Coq 2 ไฟล์ขนาด หลายแสนบรรทัด ซึ่งแต่ละไฟล์ครอบคลุมทั้ง crate - แม้จะยืนยันได้ว่าเครื่องมือนี้ทำงานกับโค้ดเบสขนาดใหญ่ได้ แต่โค้ด Coq ที่สร้างขึ้นยังไม่สามารถคอมไพล์ได้ทันที
- ข้อผิดพลาดไม่ได้เกิดถี่มากนัก แต่อยู่ในระดับประมาณหนึ่งจุดต่อทุก ๆ หลายพันบรรทัด
- ขนาดของโค้ด Rust ต้นทางตาม
clocมีดังนี้alloc: โค้ด Rust 26,299 บรรทัดcore: โค้ด Rust 54,192 บรรทัด
- เนื่องจากมีการ ขยายแมโคร ระหว่างกระบวนการแปล เป้าหมายที่แปลจริงจึงมีขนาดใหญ่กว่าจำนวนบรรทัดต้นฉบับ
การแยกโค้ด Coq ที่สร้างขึ้น
- การเปลี่ยนแปลงที่ใหญ่ที่สุดคือการแยกผลลัพธ์ของ
coq-of-rustให้เป็นไฟล์ Coq หนึ่งไฟล์ต่อไฟล์ Rust ต้นทางหนึ่งไฟล์ - การแยกนี้ทำได้เพราะการแปล ไม่ไวต่อ ลำดับของนิยาม และมีลักษณะ context-free
- โดยทั่วไปแล้ว ระหว่างไฟล์ Rust มักมีการพึ่งพาแบบวนกลับ ซึ่ง Coq ไม่อนุญาต แต่แนวทางการแปลนี้ยังคงแยกระดับไฟล์ได้
- หลังแยกแล้ว ขนาดผลลัพธ์มีดังนี้
alloc: ไฟล์ Coq 54 ไฟล์, โค้ด Coq 171,783 บรรทัดcore: ไฟล์ Coq 190 ไฟล์, โค้ด Coq 592,065 บรรทัด
- การแยกไฟล์ทำให้การสำรวจและดูแลรักษาโค้ดที่สร้างขึ้นง่ายขึ้น
- คอมไพล์แบบ ขนาน ได้ง่าย
- โฟกัสดีบักทีละไฟล์ได้
- ตัดไฟล์ที่คอมไพล์ไม่ผ่านออกได้ง่าย
- ติดตาม diff ของไฟล์เดี่ยวได้ง่าย
การแก้ปัญหาชื่อโมดูลชนกันและไฟล์ที่ยังเหลือ
- บั๊กบางส่วนเกิดจาก ชื่อโมดูลชนกัน ในบล็อก
impl - วิธีแก้คือใส่ข้อมูลในชื่อโมดูลให้มากขึ้นเพื่อเพิ่มความเป็นเอกลักษณ์
- โดยเพิ่มข้อมูลจากเงื่อนไข
whereที่ก่อนหน้านี้ยังไม่มี - ตัวอย่างเช่น ในการติดตั้ง trait
Defaultให้กับMapping<K, V>จะมีการสะท้อนเงื่อนไขที่ทั้งKและVต้องติดตั้งDefaultด้วยลงไปในชื่อโมดูล
- โดยเพิ่มข้อมูลจากเงื่อนไข
- ปัจจุบันไฟล์ที่ยังคอมไพล์บน Coq ไม่ผ่านมีดังนี้
alloc/boxed.vcore/any.vcore/array/mod.vcore/cmp/bytewise.vcore/error.vcore/escape.vcore/iter/adapters/flatten.vcore/net/ip_addr.v
- คิดเป็น 4% ของไฟล์ทั้งหมด
- อย่างไรก็ตาม แม้ในไฟล์ที่คอมไพล์ได้ ก็ยังมีองค์ประกอบของ Rust ที่ระบบยังจัดการไม่ได้และถูกทำให้เป็นสัจพจน์อยู่ ดังนั้นสัดส่วนนี้เพียงอย่างเดียวจึงยังใช้ตัดสินขอบเขตที่ยังไม่รองรับทั้งหมดไม่ได้
ตัวอย่างการแปล Option::unwrap_or_default
Option::unwrap_or_defaultของ Rust จะคืนค่าxเมื่อเป็นSome(x)และจะเรียกT::default()เมื่อเป็นNonecoq-of-rustแปลสิ่งนี้เป็นนิยาม Coq ในรูปแบบ monadic- จับคู่กับอาร์กิวเมนต์ขาเข้าและชนิดข้อมูล
- ในแขนง
Someจะดึง tuple field มาและคัดลอกค่า - ในแขนง
Noneจะเรียกเมธอดdefaultของ traitcore::default::Default
- ในการตรวจพิสูจน์จริง จะใช้ นิยามเชิงฟังก์ชัน ที่ง่ายกว่านี้แทนนิยามที่สร้างอัตโนมัติ
- หากเป็น
Noneจะคืนcore.simulations.default.Default.default - หากเป็น
Some xจะคืนx
- หากเป็น
- การพิสูจน์ว่านิยามที่สร้างอัตโนมัติกับนิยามแบบง่ายมีความสมมูลกันอยู่ใน
CoqOfRust/core/proofs/option.v - หากโค้ด Rust ต้นฉบับเปลี่ยนไป การพิสูจน์นี้จะช่วยจับความเปลี่ยนแปลงได้
- เนื่องจากการแปลไลบรารี
coreทำแบบอัตโนมัติ จึงอาจเชื่อถือในนิยามที่สร้างขึ้นได้มากกว่านิยามที่เขียนด้วยมือ - อย่างไรก็ตาม
coq-of-rustเองก็อาจมีข้อผิดพลาดหรือความไม่สมบูรณ์อยู่ จึงจำเป็นต้องตรวจสอบความถูกต้องของโค้ดในขั้นตอนการพิสูจน์ด้วย
โจทย์ที่ยังเหลือ
- ความเชื่อมั่นต่อการทำให้ไลบรารีมาตรฐานเป็นรูปนัยเพื่อใช้ตรวจพิสูจน์โปรแกรม Rust เพิ่มสูงขึ้น
- เป้าหมายถัดไปยังคงเป็นการทำให้ กระบวนการพิสูจน์ที่น่าเบื่อซ้ำซาก ง่ายลง
- โดยเฉพาะ การแสดงให้เห็นว่าซิมูเลชันสมมูลกับโค้ด Rust ต้นฉบับยังต้องอาศัยงานต่อไปนี้
- การแก้ชื่ออ้างอิง
- การนำชนิดข้อมูลระดับสูงเข้ามาใช้
- การกำจัดผลข้างเคียง
- ทิศทางการปรับปรุงต่อจากนี้คือการแยกจัดการแต่ละขั้นตอนเหล่านี้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
น่าประทับใจจริง ๆ
การแปลอัตโนมัติแบบนี้ย้ายเป้าหมายของความเชื่อถือไปอยู่ที่เครื่องมือ ตัว coq-of-rust เองเขียนด้วย Rust ไม่ใช่ Coq โครงสร้างแบบเวียนกลับจึงค่อนข้างน่าทึ่ง แต่ถ้าผสมแนวทางแบบ CompCert เข้ากับวิธีเลี่ยงการโจมตี Trusting Trust ของ Ken Thompson ด้วยคอมไพเลอร์ตัวที่สอง ตาม “Countering Trusting Trust through Diverse Double-Compiling” (2009) [0] ของ David A. Wheeler ก็ดูเหมือนจะพิสูจน์ความถูกต้องได้
ในการตรวจสอบ ก็แปลงตัวแปล coq-of-rust จาก Rust เป็น Coq ด้วย coq-of-rust และแม้การแปลนั้นทำด้วย Rust จึงไม่จำเป็นต้องเชื่อถือ ก็พิสูจน์คุณสมบัติความถูกต้องที่ต้องการภายใน Coq ได้ โดยเฉพาะว่ามันรักษาความหมายไว้เมื่อแปลโปรแกรม Rust เป็น Coq
อย่างในบทความ มีโอกาสสูงว่าการพิสูจน์ด้วยนิยามที่เป็นเชิงฟังก์ชันมากกว่านิยามที่สร้างออกมาจะง่ายกว่า ดังนั้นก็ทำกระบวนการพิสูจน์ความเทียบเท่าระหว่างนิยาม เหมือนที่ทำในไลบรารีมาตรฐานได้ ถ้าตัวแปล coq-of-rust ปัจจุบัน โดยเฉพาะ lib/ [1] มี Rust 6,350 บรรทัด การเขียนตัวแปลทั้งหมดเป็น Coq แล้วพิสูจน์ว่าเทียบเท่ากับสิ่งที่สร้างออกมาก็ดูเป็นไปได้จริง
จากนั้นเมื่อนำตัวแปล coq-of-rust เวอร์ชัน Coq ที่พิสูจน์แล้วไปรันกับซอร์ส coq-of-rust ที่เขียนด้วย Rust นิยาม Coq ที่ได้ออกมาควรตรงกับเอาต์พุตของตัวแปล coq-of-rust เวอร์ชัน Rust ที่ใช้ในตอนแรก
นอกเรื่องเล็กน้อย การได้เห็นงานแบบนี้มีเงินทุนจากอุตสาหกรรมสนับสนุนเป็นเรื่องดี แม้ผมจะค่อนข้างเย้ยหยันคริปโตเคอร์เรนซี แต่ก็จริงที่ความต้องการด้านความถูกต้องของวงการนั้นช่วยผลักดันการพัฒนาในสิ่งที่ผมสนใจ เช่น Rust, Coq และการสนับสนุนนักศึกษาปริญญาโทที่รู้จัก
[0]: https://dwheeler.com/trusting-trust/wheelerd-trust.pdf
[1]: https://github.com/formal-land/coq-of-rust/tree/main/lib
แต่แม้ตัวโค้ดเองจะสั้น ก็ยังพึ่งพาคอมไพเลอร์ Rust ในการพาร์สและตรวจชนิดของโค้ด Rust ที่รับเข้ามา ดังนั้นส่วนนั้นก็ต้องตรวจสอบด้วย หรืออย่างน้อยต้องมีสเปกเชิงรูปแบบแม้จะยังไม่มีการพิสูจน์ สิ่งที่เป็นอุปสรรคคือ API ของ rustc ค่อนข้างใหญ่และไม่เสถียร แต่ถึงอย่างนั้นก็อาจเป็นวิธีหนึ่งในการเพิ่มระดับความเชื่อถือได้
แน่นอนว่ามีปัญหาเรื่องต้องตรวจสอบเอาต์พุตหรือตัวแปลง แต่โค้ด C ที่ได้ค่อนข้างอ่านง่ายเพื่อวัตถุประสงค์ในการตรวจสอบ และมีสไตล์ที่จำกัด รวมถึงมีความเชื่อมั่นต่อเครื่องมือสูงด้วย การวิเคราะห์เชิงสถิตของ SPARK เป็นเพียงส่วนหนึ่งของกระบวนการตรวจสอบและยืนยันทั้งหมด และการทดสอบกับกิจกรรมอื่น ๆ ก็ให้ชั้นความเชื่อมั่นเพิ่มเติม โดยรวมแล้วเป็นแนวทางที่ใช้ได้ผลค่อนข้างดี
ที่พวกเขาใช้เงินนั้นกับการวิจัยวิทยาการคอมพิวเตอร์ ไม่ใช่แค่เพราะเป็นประโยชน์ต่อตัวเอง แต่ยังเป็นการทำกุศลที่เชื่อมโยงกับงานอดิเรกของพวกเขาด้วย
อีกแนวทางหนึ่งคือโค้ดที่พกพาการพิสูจน์มาด้วย ให้คอมไพเลอร์ Rust ส่งออกการพิสูจน์ใน Coq แนบมาด้วยว่าเอาต์พุตไฟล์ปฏิบัติการสอดคล้องกับความหมายของซอร์สโค้ดอินพุต
แน่นอนว่าอาจเขียนคอมไพเลอร์สำหรับซับเซตของ Rust เช่นไม่มี borrow checker หรือการปรับแต่งประสิทธิภาพ ให้เป็นภาษาเครื่องของสถาปัตยกรรมเฉพาะ แล้วบูตสแตรปทุกอย่างจากตรงนั้นก็ได้
โปรแกรมที่ตรวจสอบตั้งแต่ต้นจนจบด้วย ระบบพิสูจน์อนุมานกึ่งอัตโนมัติอย่าง Coq มักมีขนาดเล็ก ไลบรารีอาจง่ายกว่าเพราะโค้ดมีระดับการโต้ตอบกันต่ำกว่า แต่โปรแกรมทั่วไปไม่เป็นเช่นนั้น
จริง ๆ แล้วการเพิ่มขึ้นของขนาดโปรแกรมที่ตรวจสอบได้ด้วยวิธีนี้เติบโตช้ากว่าการเพิ่มขึ้นของขนาดเฉลี่ยของโปรแกรมทั้งหมด การตรวจสอบซอฟต์แวร์แบบ sound คือการแสดงว่าเป็นไปตามสเปก 100% นั้นมีประโยชน์ชัดเจนในงานอย่างการพิสูจน์ความถูกต้องของอัลกอริทึมหลัก แต่ขยายขนาดได้ไม่ดี
ด้วยเหตุนี้ งานวิจัยจึงมีแง่มุมที่ย้ายไปสู่วิธีแบบไม่ sound มากขึ้น ต้นทุนของการรับประกัน 100% อาจเป็น 10 เท่าของการรับประกัน 99.9999% และส่วนต่างของความน่าจะเป็นนั้นอาจเล็กกว่าความน่าจะเป็นของความล้มเหลวนอกซอฟต์แวร์ด้วยซ้ำ ระบบกายภาพตั้งแต่แรกก็พิสูจน์ไม่ได้ว่าเป็นไปตามสเปก และตัวสเปกเองก็มีความน่าจะเป็นที่จะไม่ตรงกับความเป็นจริงเพียงพอ
การพิสูจน์ว่าเฉพาะโค้ด unsafe ถูกต้องนั้นทำได้โดยใช้ความพยายามน้อยกว่าการพิสูจน์โค้ด Rust ทั้งหมดมาก นี่เป็นคำตอบต่อคำวิจารณ์ที่มักเกิดขึ้นเมื่อพูดถึงการรับประกันความปลอดภัยของ Rust ว่า “แล้วโค้ด unsafe ล่ะจะทำอย่างไร” ช่องว่างที่ระบบชนิดของ Rust ยังไม่ทรงพลังพอจะจัดการได้ สามารถเติมเต็มด้วยระบบที่แข็งแรงกว่าอย่าง Coq
มีจุดหนึ่งในความพยายามแบบนี้ที่ยังไม่ค่อยเข้าใจ ถ้าต้องแปลงโค้ดเป็น Coq ด้วยมือหรือกึ่งอัตโนมัติ ความเป็นไปได้ที่จะทำพลาดในกระบวนการนั้นไม่มากกว่าประโยชน์ที่ได้จากการตรวจสอบเชิงรูปแบบมากหรือ?
กล่าวอีกอย่างคือ เราจะรู้ได้อย่างไรว่าสิ่งที่เราพิสูจน์นั้นยังใช้ได้กับ โค้ดต้นฉบับ?
การตรวจสอบเชิงรูปแบบมักถูกพูดถึงเหมือนกับว่าจะกำจัดความเป็นไปได้ของบั๊กได้ทั้งหมด แต่ในความเป็นจริงใกล้เคียงกับการลดโอกาสเกิดบั๊กลงอย่างมากมากกว่า ถึงอย่างนั้น การแปลอัตโนมัติระหว่าง Rust กับ Coq ก็ลดความซับซ้อนของสิ่งที่ต้องเชื่อถือลงได้มาก จึงควรถูกเลือกใช้มากกว่าการแปลด้วยมือ
ในหลายกรณี บั๊กในการแปลจะทำให้พิสูจน์ไม่ได้เฉย ๆ แล้วก็จะมีคนสืบว่าทำไมการพิสูจน์ถึงไม่ผ่าน จนพบว่ามีบั๊กในการแปล
ปัญหาจริง ๆ คือกรณีที่บั๊กในการแปลไปหักล้างบั๊กในโค้ดต้นฉบับได้พอดี หากไม่มีความเสี่ยงเชิงระบบ การที่บั๊กสองตัวจะลบล้างกันแบบนี้ถือว่ามีโอกาสค่อนข้างต่ำ
สำหรับผู้อ่านที่สนใจ: ผมส่งบทความบล็อกนี้มาเพราะเนื้อหาเกี่ยวกับคริปโทเคอร์เรนซีโดยตรงน้อยกว่าบทความอื่น ๆ ในบล็อกเดียวกัน แต่ที่นั่นมีบทความที่น่าสนใจเชิงเทคนิคอีกมาก
โดยเฉพาะสองบทความล่าสุดที่พูดถึงการนำแนวทางเดียวกันนี้ไปใช้กับ Python แทนที่จะเป็น Rust
จำได้ว่ามีการบรรยาย [1] เมื่อก่อนเกี่ยวกับ fuzzer ที่พบบั๊กใน คอมไพเลอร์ C ที่ตรวจสอบเชิงรูปแบบแล้ว เพราะการตรวจสอบเชิงรูปแบบไม่ได้ครอบคลุม frontend และ backend
ผมเข้าใจว่ามีคำถามว่าเราจะเชื่อถือ Coq เองหรือการแปลได้แค่ไหน และก็สงสัยด้วยว่าจะซิงก์กับการอัปเดตของ Rust อย่างไร แต่แม้แต่การตรวจสอบเชิงรูปแบบที่สมบูรณ์แบบก็ไม่ได้หมายความว่าถูกต้อง 100% ตั้งแต่ต้นจนจบ
[1] https://youtu.be/Ux0YnVEaI6A
ผมไม่ใช่ผู้เชี่ยวชาญด้านการตรวจสอบเชิงรูปแบบเลย แต่ถ้าไลบรารีพื้นฐานของ Rust ถูกตรวจสอบเชิงรูปแบบแล้ว และไม่ใช้โค้ด unsafe โปรแกรม Rust ทุกตัวที่ใช้ไลบรารีที่ถูกตรวจสอบเชิงรูปแบบก็จะมี คุณภาพระดับการตรวจสอบเชิงรูปแบบ ในแง่การจัดการหน่วยความจำโดยแทบจะเป็นเช่นนั้นใช่ไหม?
Rust มีนิยาม “safe” ของตัวเอง และถือได้ว่าเป็นส่วนย่อยของ memory safety แม้แต่โค้ด Rust ที่ปลอดภัยทั้งหมดก็ยังเกิด data race, deadlock, memory exhaustion ฯลฯ ได้ ยังไม่ต้องพูดถึงข้อผิดพลาดทางตรรกะ
อย่างแรก ต้องทำให้ semantics ของ unsafe Rust เป็นรูปแบบทางการ งาน RustBelt[1] อันบุกเบิกของ Ralf Jung เป็นความก้าวหน้าครั้งใหญ่ แต่ยังไม่สมบูรณ์ โดยเฉพาะ pointer provenance กลายเป็นองค์ประกอบที่ยุ่งยาก
อย่างที่สอง ในฐานะส่วนหนึ่งของเรื่องนั้น จำเป็นต้องมีโมเดลเชิงรูปแบบของ borrow checker Stacked borrows[2] เป็นความพยายามที่ดีแต่มีข้อบกพร่อง และ Tree borrows[3] อาจแก้ได้ หรืออาจมีบางอย่างที่ละเอียดกว่านั้นออกมาก็ได้
อย่างที่สาม ต้องมีโมเดลหน่วยความจำเชิงรูปแบบ เรื่องนี้ส่วนใหญ่เกี่ยวกับพฤติกรรมของ atomic operations และ synchronization จึงสำคัญมากต่อองค์ประกอบของ standard library อย่าง Arc เป็นที่ยอมรับกันกว้างขวางว่าโมเดลหน่วยความจำของ Rust ควรคล้ายกับ C++ และต้องทำงานร่วมกันได้ แต่ยังเหลือปัญหา “out of thin air” และฟีเจอร์ที่ขาดไปอย่าง seqlock อยู่ นี่เป็นหนึ่งในเหตุผลที่ Linux kernel ยังใช้โมเดลของตัวเอง
อย่างที่สี่ ต้องมีการพิสูจน์ว่าการรับประกันความปลอดภัยประกอบกันได้ดี โดยเฉพาะเมื่อโค้ดที่ sound ซึ่งเขียนด้วย unsafe Rust ถูกประกอบเข้ากับโค้ด safe Rust แล้ว การรับประกันความปลอดภัยต้องยังคงอยู่ จนถึงตอนนี้มีผลลัพธ์ที่ดี แต่ต้องพิสูจน์สำหรับทั้งระบบ
อย่างที่ห้า เพื่อให้การพิสูจน์แบบนั้นใช้ได้กับทุกโค้ด ต้องปิดบั๊กด้าน soundness ที่ยังเหลืออยู่ใน Rust[1] ทั้งหมด ปัญหาจำนวนมากในกลุ่มนี้มีความสำคัญจริง ๆ แค่ในเชิงทฤษฎีหรือในโค้ดแบบมุ่งร้าย[5] ทำให้ความคืบหน้าช้า
แม้ทั้งหมดนี้เสร็จแล้ว ก็ยังเป็นเพียงการรับประกันบางส่วนเท่านั้น พื้นผิวจำนวนมหาศาลที่ระบบเชื่อมต่อด้วยอิงอยู่กับโค้ด unsafe ถ้าทำแค่การคำนวณล้วน ๆ ก็อีกเรื่อง แต่ถ้าสร้าง UI กราฟิกเป็นต้น ก็ยังมีสิ่งที่ผิดพลาดได้มากมาย
ถึงอย่างนั้นก็ยังมีเส้นทางเดินหน้าที่ใช้งานได้จริง และมันจะนำไปสู่สถานการณ์ที่ดีกว่าสภาพทั่วไปของระบบที่เต็มไปด้วยช่องโหว่ในปัจจุบันมาก
[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 ทั้งหมดได้
แนวทางนี้ต่างจาก Aeneas หรือ RustHornBelt อย่างไรบ้าง? แล้วจัดการพอยน์เตอร์กับการยืมแบบแก้ไขได้อย่างไร?
เมื่อเทียบกับ Aeneas เป้าหมายคล้ายกันมาก เพราะทั้งสองพยายามตรวจพิสูจน์โปรแกรม Rust ด้วยตัวพิสูจน์ทฤษฎีบทแบบโต้ตอบได้ แต่ใน coq-of-rust เราจะเขียนโค้ดเวอร์ชันฟังก์ชันบริสุทธิ์ที่จะเป็นเป้าหมายของการพิสูจน์ด้วยมือ หรือถ้าเป็นงานซ้ำ ๆ ก็ใช้ความช่วยเหลือจาก GitHub Copilot แล้วพิสูจน์ว่าโค้ดนั้นสมมูลกับผลลัพธ์ที่แปลอัตโนมัติ ส่วน Aeneas ตั้งเป้าที่จะสร้างเวอร์ชันฟังก์ชันโดยตรง
พอยน์เตอร์ทั้งหมดถูกจัดการเหมือนเป็นพอยน์เตอร์ที่แก้ไขได้ กล่าวคือเป็นชนิด
*เราไม่ได้ใช้ข้อมูลจาก borrow checker ของ Rust ซึ่งทำให้การแปลง่ายขึ้น แต่ต้องไปจ่ายต้นทุนนี้ในขั้นตอนการพิสูจน์เพื่ออนุมานพอยน์เตอร์ในการพิสูจน์ เราให้ผู้ใช้จัดเตรียมตัวจัดสรรแบบกำหนดเองที่ออกแบบได้ตามวิธีที่หน่วยความจำจะถูกใช้งาน ตัวอย่างเช่น ถ้าโปรแกรมใช้ตัวแปรโกลบอลแบบแก้ไขได้สามตัว ก็อาจกำหนดหน่วยความจำเป็นเรคอร์ดที่มีสามรายการได้ รายการเหล่านี้ตอนแรกจะเป็น
Noneเพื่อแทนสถานะที่ยังไม่ได้จัดสรรเรายังไม่รู้ว่าเทคนิคนี้จะขยายสเกลได้แค่ไหน แต่อย่างน้อยตอนนี้ก็หลีกเลี่ยงการให้เหตุผลด้วย separation logic ได้ เราคาดว่าโปรแกรมส่วนใหญ่ที่ต้องการตรวจพิสูจน์ โดยเฉพาะฝั่งแอปพลิเคชัน จะมีวินัยการใช้หน่วยความจำที่ค่อนข้าง “เรียบง่าย”
การเขียนสเปกสำหรับการตรวจพิสูจน์เชิงรูปแบบ คล้ายกับการใช้ property-based testing ที่ซับซ้อนขึ้นหรือไม่? พอเลยโปรแกรมที่ไม่ซับซ้อนไปแล้ว การเขียน property-based test ก็ค่อนข้างยากและใช้เวลามากเหมือนกัน
เมื่อตรวจพิสูจน์เชิงรูปแบบในระดับเดียวกัน ก็อาจดูคล้ายกันมาก แต่เครื่องมือสำหรับการตรวจพิสูจน์เชิงรูปแบบสามารถลงลึกกว่าได้ เช่น ตอบคำถามเกี่ยวกับสถานะของระบบภายในระหว่างการคำนวณได้ว่า “มี loop invariant นี้อยู่ พิสูจน์ได้ไหมว่ามันคงอยู่จริงในทุกรอบ?” หรือ “พิสูจน์ได้ไหมว่าไม่มี underflow/overflow เกิดขึ้นเลยในการคำนวณขั้นกลางใด ๆ ระหว่างการคำนวณนี้?”
กรณีแรกทำด้วย property-based testing ได้เช่นกัน หากแยกแกนของลูปออกมาเป็นโพรซีเยอร์ต่างหาก แล้วรันกับสถานะกลางจำนวนมากเพื่อทดสอบคุณสมบัติ invariant ส่วนกรณีหลังยากกว่ามาก เว้นแต่จะแตกโปรแกรมออกเป็นชิ้นเล็กมากจนแทบจะรันแยกได้ทีละบรรทัด
ไม่เคยรู้มาก่อนเลยว่าสิ่งแบบนี้เป็นไปได้
สงสัยว่าความพยายามแบบนี้จะช่วยเร่งให้ Rust ถูกนำไปใช้ ในส่วนสำคัญของการนำเข้าเคอร์เนลได้หรือไม่
ใครช่วยอธิบายแนวคิด “การตรวจพิสูจน์” แบบง่ายมาก ๆ ได้ไหม? อยากรู้ว่าทำไมถึงต้องมีภาษาทั้งภาษาอย่าง Coq อยู่เพื่อจุดประสงค์นี้โดยเฉพาะ และมันมีความหมายเชิงปฏิบัติอย่างไรต่อสังคมในวงกว้าง