2 คะแนน โดย GN⁺ 2024-12-22 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Scylla เสนอเส้นทางที่ทำให้คอมไพล์เป็น Rust ที่ปลอดภัย ได้โดยตรง ด้วยการค่อย ๆ จัดโครงสร้าง C ต้นฉบับ แทนที่จะย้าย C เดิมไปเป็น unsafe Rust แล้วค่อยแก้
  • เป้าหมายคือ ชุดย่อย applicative C ที่รวมถึงการประมวลผลข้อมูล การดำเนินการกับ pointer, control flow ที่มีโครงสร้าง และโค้ดที่พกพาได้ โดยไม่รวมโค้ดที่พึ่งพา goto, การ cast ระหว่าง integer-pointer, เทคนิค pointer, bitfield และ untagged union
  • การแปลงมี 2 ขั้นตอน คือ ลดระดับ Clang AST ลงเป็น Mini-C แล้วจึงย้ายไปเป็น safe Rust พร้อมทำให้พฤติกรรมละเอียดอ่อนของ C ชัดเจนขึ้น เช่น integer promotion, implicit conversion, assignment expression และ prefix/postfix increment
  • ส่วนที่ยากที่สุดคือ pointer arithmetic ซึ่งถูกเปลี่ยนเป็นการแบ่งส่วนโดยใช้ Rust slice และ split_at_mut/split_at พร้อมจัดการ mutable inference, การ derive trait และการเลือกใช้ Box กับ borrow ด้วย
  • มีการประเมินกับ Windows SymCrypt, HACL★, อัลกอริทึมบีบอัดหลักของ bzip2, parser/serializer ของ EverParse CBOR และบางส่วนของ Microsoft FrodoKEM และยังพบ undefined behavior ใน bzip2 กับ FrodoKEM ระหว่างการแปลงด้วย

เส้นทางการย้ายจาก C→Rust ที่ Scylla เลือก

  • แรงจูงใจในการย้ายโค้ด C เดิมไปเป็น Rust เริ่มจากปัญหา ความปลอดภัยของหน่วยความจำ
    • งานวิจัยของ Google และ Microsoft ประเมินว่า 70% ของช่องโหว่ด้านความปลอดภัยเกี่ยวข้องกับการจัดการหน่วยความจำที่ผิดพลาด
    • บริษัทและรัฐบาลกำลังแนะนำให้ใช้ภาษาที่ปลอดภัยด้านหน่วยความจำอย่าง Rust ในระบบที่มีความสำคัญด้านความปลอดภัย
  • Rust มีข้อได้เปรียบชัดเจนสำหรับโค้ดใหม่ แต่การเขียนโค้ด C ระดับอุตสาหกรรมที่ผ่านการทดสอบและดีบักมาแล้วทั้งหมดใหม่อีกครั้งนั้นทำได้ยาก
  • เครื่องมือแปลง C→Rust อัตโนมัติที่มีอยู่มักสร้าง unsafe Rust เพื่อรองรับ C ทั้งหมด
    • อนุญาตสำนวนแบบ C เช่น unchecked pointer หรือ transmutation ของ Rust
    • การรับประกันความปลอดภัยของหน่วยความจำแบบ static จึงหายไป ทำให้เป้าหมายของการใช้ภาษาที่ปลอดภัยอ่อนลง
  • workflow ทั่วไปคือใช้ผลลัพธ์ unsafe Rust เป็นจุดเริ่มต้น แล้วค่อย refactor ซ้ำ ๆ ให้เป็น Rust ที่ปลอดภัย
    • มีการเสนอ static analysis เพื่อเปลี่ยน raw pointer เป็น borrow ของ safe Rust หรือกู้คืน abstraction ของ Rust จาก representation ระดับต่ำ
    • เครื่องมือ refactor กระจัดกระจาย และการรองรับ c2rust refactor ก็หยุดลงในปี 2022
  • Scylla เลือกแนวทางที่ค่อย ๆ จัดโครงสร้างตัวโค้ด C ต้นฉบับเอง ให้คอมไพล์เป็น safe Rust ได้ แทนที่จะแก้ unsafe Rust ที่สร้างออกมา

ชุดย่อยของ C ที่รองรับและ pattern ที่ไม่รองรับ

  • เป้าหมายของ Scylla คือการแปลงที่คาดเดาได้ และการสร้างโค้ด Rust ที่ใกล้เคียงกับ C ต้นฉบับ
  • สิ่งที่รองรับคือ ชุดย่อย applicative C
    • โค้ดที่จัดการและประมวลผลข้อมูล
    • โค้ดที่ใช้ pointer arithmetic
    • โค้ดที่มี control flow แบบมีโครงสร้าง
    • โค้ดที่พกพาได้
  • ไม่รองรับ codebase ที่พึ่งพา pattern ต่อไปนี้
    • goto
    • การใช้ representation ของ object ผ่านการ cast ระหว่าง integer-pointer
    • เทคนิค pointer
    • bitfield
    • untagged union
  • นักพัฒนาสามารถใส่ targeted rewrite และ annotation ในซอร์ส C เพื่อให้ Scylla เข้าใจได้
    • สามารถเขียน pattern ของ aliasing ที่ไม่เข้ากับ borrow checker ของ Rust ใหม่ได้
    • สามารถให้ข้อมูลกับ Scylla ว่า tagged union ควรถูกแปลเป็น ADT ระดับสูง

Mini-C: ภาษากลางที่ลดความกำกวมของ C

  • Scylla เริ่มจาก AST ของ Clang frontend แล้วแปลงเป็นภาษาที่ชื่อ Mini-C ก่อน
  • Mini-C จัดการ branch, loop, pointer, dereference และการนำ address มาใช้เหมือน C แต่มี semantics แบบ “no-surprises”
    • integer ทั้งหมดมีความกว้างคงที่
    • integer promotion และ integer conversion ของ C แสดงด้วย explicit cast
    • ไม่อนุญาต untyped pointer อย่าง void *
  • Mini-C ต่างจาก C ตรงที่เป็น ภาษาแบบ expression
    • assignment ไม่คืนค่า
    • syntax ของ C เช่น e1 = e2 = e3, p[i++] จะถูก desugar ใน Mini-C
    • expression ที่ใช้ทดสอบใน loop และ conditional statement ต้องมีชนิดเป็น bool ไม่ใช่ int ของ C
  • ระหว่างย้าย typed AST ของ Clang ไปเป็น typed AST ของ Mini-C จะทำให้พฤติกรรม implicit ของ C ชัดเจนขึ้น
    • ปรับ conditional expression ให้เป็น bool
    • ปรับ array index ให้เป็น size_t
    • เปลี่ยน implicit conversion ของ argument ในการเรียกฟังก์ชันและด้านขวาของ assignment เป็น explicit cast
    • ใน arithmetic operation จะสะท้อน usual arithmetic conversions ตามมาตรฐาน C
  • การแปลงตั้งสมมติฐานว่าโค้ด C พกพาได้ และไม่พึ่งพา data model ของ C
    • เช่น ไม่คาดหวังโค้ดที่พฤติกรรมเปลี่ยนตามว่า long มีขนาด 4 ไบต์หรือ 8 ไบต์
    • implementation จะตรวจจับ data model ของสถาปัตยกรรมเป้าหมายในช่วง configure-time แล้วแปลง unsigned int เป็นชนิดความกว้างคงที่อย่าง uint32_t

การสังเคราะห์ ADT และ tuple

  • Mini-C มี ADT, tuple และ pattern matching ในระดับที่สูงกว่า C
  • tagged union ถูกแปลเป็น ADT ผ่าน annotation
    • รูปแบบเป้าหมายคือ struct แบบ { int tag; union { t0 case0; ...; tn caseN }}
    • สมมติว่า tag value อยู่ตั้งแต่ 0 ถึง N และ tag value ตรงกับลำดับของ union case
  • Scylla แปลงชนิด annotated tagged union เป็นชนิด variant
    • ตอนสร้างค่า จะตรวจสอบว่า .tag = i และ .casej = e สอดคล้องกันหรือไม่
    • หากตรงกัน จะแปลงเป็นค่าของ constructor นั้น
    • หาก payload กับ tag ไม่ตรงกัน จะไม่แปลเป็น Mini-C
  • การเข้าถึง field ของ tagged union จะปลอดภัยได้ก็ต่อเมื่อรู้สถานะ tag ปัจจุบัน
    • จดจำ pattern อย่าง if (x.tag == i) { ... x.casei } หรือ switch
    • แปลงเป็นรูปแบบ match x with | Ci v -> ...
    • การเข้าถึง union case อื่นถือว่า invalid และทำให้เกิดข้อผิดพลาดในการแปลง
  • tuple ก็สังเคราะห์ได้ด้วย annotation
    • struct ที่มี n field จะถูกแปลงเป็น n-ary tuple
    • การเข้าถึง field จะเปลี่ยนเป็น tuple field access
    • tuple มี type แบบ structural จึงได้ประโยชน์จาก mut-polymorphism

การแปลงจาก Mini-C ไปเป็น safe Rust

  • Mini-C ให้ representation ของโปรแกรม C ที่มี type annotation ครบถ้วน จากนั้นจึงถูกแปลงเป็น safe Rust
  • ความยากหลักมีสามด้าน
    • การกำจัด pointer arithmetic ของ C
    • การทำให้ mutability และ aliasing ชัดเจน
    • การจัดเตรียมโครงสร้างตามสำนวน Rust เช่น trait โดยอัตโนมัติ
  • การแปลงชนิด pointer ซับซ้อนเพราะ Rust มี representation ของ pointer ที่ต่างกัน
    • Rust แยก Box<T> กับ &T
    • pointer ไปยัง element เดียวและหลาย element ก็แยกกัน เช่น &T กับ &[T]
    • array ของ Rust เป็นค่า และไม่ decay เป็น pointer โดยอัตโนมัติเหมือน C
  • กลยุทธ์พื้นฐานคือคอมไพล์ pointer type ทั้งหมดของ C เป็น slice borrow ของ Rust อย่าง &[T]
    • ทั้ง stack pointer และ heap pointer จะกลายเป็น slice borrow โดยพื้นฐาน
    • pointer ของ element เดียวและหลาย element ก็จะเป็น slice borrow โดยพื้นฐานเช่นกัน
    • mutability จะถูก infer อัตโนมัติในขั้นตอนแยกต่างหาก
  • Scylla ใช้ heuristic และ annotation แบบ manual เพื่อแปล pointer บางส่วนเป็น Box<T>
    • ฟังก์ชันอย่าง T *create() ที่ไม่มี global reference และถูกตัดสินว่าเป็น fresh allocation อาจถูกแปลเป็น fn create() -> Box<T>
    • analysis นี้ถูกนำไปใช้แบบ recursive ภายใน definition ของ struct และ variant ด้วยวิธี fixed point
    • struct ที่ยังมี borrow เหลืออยู่จะมี lifetime parameter

ข้อจำกัดของการแปลง Box, slice และ array

  • ใน Rust ต้องแปลงระหว่าง array, slice borrow และ Box อย่างชัดเจน ดังนั้นการแปลง Rust ของ Scylla จึงทำงานแบบ type-directed ด้วย
  • กฎการแปลงจะใส่ coercion ที่เปลี่ยน array หรือ boxed slice เป็น slice borrow
    • array จะกลายเป็น slice borrow ในรูปแบบอย่าง &x[..]
    • boxed slice สามารถแปลงเป็น borrow ได้
  • การแปลงย้อนทิศทางก็ทำได้เช่นกัน
    • สามารถยกระดับ slice หรือ array เป็น heap allocation แล้วเปลี่ยนเป็น Box<[T]> ได้
  • การแปลงย้อนทิศทางนี้อาจสร้างความแตกต่างด้าน copy semantics
    • ใน C array กับ pointer อาจชี้ไปยัง memory เดียวกัน
    • ใน Rust, Box::new(x) อาจสร้าง copy ของ x
    • array ของ primitive type อย่าง integer array ไม่มีวิธี opt out จาก trait Copy ทำให้ Rust อาจทำ copy แบบเงียบ ๆ
  • Scylla จะลบตัวแปรเดิมออกจาก environment เมื่อเกิดการแปลงลักษณะนี้ เพื่อห้ามใช้งานต่อ
    • หากโปรแกรม C ต้นฉบับยังใช้ตัวแปรนั้นต่อ จะเกิดข้อผิดพลาดในการแปลง
    • นักพัฒนาควรแก้ซอร์ส C ก่อนแปลง เพื่อทำให้เจตนาชัดเจนขึ้น

การเปลี่ยน pointer arithmetic เป็นการแบ่ง Rust slice

  • โปรแกรม C มักไม่ได้เข้าถึง array ผ่าน base pointer เดียว แต่ใช้ pattern ที่แบ่ง array เป็น chunk หรือคง pointer ตำแหน่งปัจจุบันไว้ขณะวนผ่าน
  • Rust ไม่อนุญาต pointer arithmetic แบบอิสระ และให้วิธีแบ่ง slice ด้วย split_at_mut หรือ split_at
    • split_at_mut เป็น primitive ที่สละ ownership ของ slice เดิมแล้วได้ sub-slice สองส่วน
    • รักษา invariant ของ Rust ที่ว่า mutable data ต้องมี owner เพียงหนึ่งเดียว
  • Scylla นำ split tree มาใช้เพื่อจับคู่ pointer arithmetic ของ C กับวิธีแบ่งของ Rust
    • pointer ของ C แต่ละตัวถูก map ไปยัง split tree หนึ่งต้น
    • split tree เปลี่ยนไปตาม flow
    • ติดตามว่า ณ จุดใดของโปรแกรม การเข้าถึง pointer ของ C ควรถูกเปลี่ยนเป็นการเข้าถึง slice ของ Rust แบบใด
  • เนื่องจาก pointer ของ C ไม่มีข้อมูลความยาว Scylla จึงสมมติว่า chunk ต่าง ๆ ไม่ซ้อนทับกัน
    • หากตั้งใจให้ซ้อนทับกัน ก็จะไม่ผ่านการตรวจ type ของ Rust และนักพัฒนาต้องเขียนโค้ด C ใหม่
    • เพื่อให้การแปลงคาดเดาได้ จึงหลีกเลี่ยง backtracking และทำแบบ forward
  • ในตัวอย่าง มีการแบ่ง array 32 ไบต์ abcd เป็นพื้นที่ limb ขนาด 8 ไบต์สี่ส่วน
    • ใน C ใช้ pointer arithmetic ที่ไม่ได้เรียงจากซ้ายไปขวา เช่น abcd + 0, abcd + 16, abcd + 8, abcd + 24
    • การแปลงเป็น Rust จะเก็บประวัติของการเรียก split_at_mut ไว้ใน split tree เพื่อหา sub-slice ที่ถูกต้อง

เป้าหมายที่ประเมินและ undefined behavior ที่พบ

  • implementation ของ Scylla ใช้ Clang รับโค้ด C เดิมเป็น input และ output เป็น safe Rust
  • เป้าหมายการประเมินรวมถึงบางส่วนของโปรเจกต์ C เดิมหลายตัว
    • บางส่วนของ SymCrypt บน Windows
    • บางส่วนของไลบรารีเข้ารหัส HACL★
    • ส่วนหลักของอัลกอริทึมบีบอัด bzip2
    • binary parser และ serializer ของ CBOR ในไลบรารี EverParse
    • implementation ของ post-quantum cryptographic primitive FrodoKEM ของ Microsoft
  • กรณีเหล่านี้แสดงให้เห็นว่าชุดย่อย applicative C ของ Scylla สามารถครอบคลุมแอปพลิเคชันที่อ่อนไหวด้านความปลอดภัยได้หลายประเภท
  • ระหว่างการแปลง ยังระบุและรายงาน undefined behavior ที่มีอยู่ในโค้ด C ต้นฉบับของ bzip2 และ FrodoKEM ด้วย

1 ความคิดเห็น

 
GN⁺ 2024-12-22
ความคิดเห็นจาก Hacker News
  • สิ่งสำคัญคือ งานนี้มุ่งเป้าไปที่ “โค้ดเบส C ที่ผ่านการตรวจพิสูจน์เชิงรูปแบบแล้ว
    โค้ด C สำหรับระบบทั่วไปไม่ได้ผ่านการตรวจพิสูจน์เชิงรูปแบบ ดังนั้นจึงเป็นอีกเรื่องหนึ่งพอสมควร

    • ถึงอย่างนั้นก็ยังดูเหมือนไม่ใช่สิ่งที่เชื่อถือได้อย่างสมบูรณ์ ในหัวข้อ 2.2 ของบทความก็ระบุว่าการบังคับที่กฎการแปลงนำเข้ามาอาจทำให้เกิด ความแตกต่างด้านความหมาย ที่ละเอียดอ่อนได้
      ตัวอย่างเช่น พอยน์เตอร์ที่เคยชี้ไปยังอาร์เรย์ C บนสแตก อาจถูกแปลใน Rust เป็น Box<[u8]> ซึ่งเหมือนพอยน์เตอร์ที่เป็นเจ้าของสำเนาใหม่บนฮีป หากโค้ดเดิมพึ่งพาข้อเท็จจริงว่าพอยน์เตอร์นั้นชี้ไปยังอาร์เรย์จริง โค้ดที่แปลแล้วอาจทำงานผิดอย่างเงียบ ๆ ได้
      ถ้าเป็นฟีเจอร์แปลอัตโนมัติสำหรับส่วนย่อย C++ ที่ปลอดภัยด้านหน่วยความจำในโปรเจกต์ของผมอย่าง scpptool ก็คงจัดการด้วยการย้ายอาร์เรย์ไปเป็นชนิดทดแทนและ iterator เพื่อคงความหมายเดิมไว้
      โปรเจกต์ของ OP อาจรับมือเฉพาะ C ที่แปลงเป็น Rust ที่ปลอดภัยได้ง่าย แต่เมื่อคิดถึงความยากของปัญหาแล้ว ผลงานนี้ก็น่านับถือและดูมีประโยชน์ในระดับหนึ่ง
    • เรื่องนี้มีข้อควรระวังมากกว่านั้นเยอะ และเกือบจะเข้าข่าย โฆษณาเกินจริง
      ตั้งแต่แรก พวกเขาไม่ได้แปล C จริง ๆ แต่เปลี่ยนให้โค้ดที่เขียนด้วย F* ปล่อย Rust ออกมาทางฝั่งคอมไพเลอร์ C แทน ไม่ได้เผชิญหน้ากับโค้ด C จริงที่ซับซ้อน และอย่างมากก็จัดการกับ Mini-C แบบจำกัดที่คอมไพเลอร์ของเล่นน่าจะปล่อยออกมาเท่านั้น
      ในต้นฉบับยังบอกด้วยว่าถ้าโปรแกรม C เดิมพึ่งพา x มากกว่านั้น การแปลจะเกิดข้อผิดพลาดและขอให้โปรแกรมเมอร์แก้ซอร์ส ซึ่งหมายความว่าพวกเขาหวังให้ C ถูกเขียนไว้แล้วในสไตล์ที่ตัวตรวจสอบการยืมของ Rust พอใจ
      ดูเหมือนถ้อยคำแบบวงวิชาการที่ว่า “รูปที่ 4 นำเสนอกฎที่งดงาม แต่การใช้งานจริงพึ่งพาทริกจำนวนมาก”
      แย่กว่านั้นคือ บอกว่ากรณีการซ้อนทับที่แยกแยะได้แบบสแตติกจะทำให้คอมไพล์เออร์เรอร์ และถ้าไม่ใช่กรณีนั้น โค้ด Rust อาจ panic ระหว่างรันได้ การเปลี่ยนโปรแกรม C ที่ผ่านการตรวจพิสูจน์เชิงรูปแบบให้กลายเป็นโปรแกรม Rust ที่ “ตอนนี้อาจแครชได้” นั้นดูแปลก
      การเรียก HACL* ว่าเป็นโค้ดเบส C ที่ผ่านการตรวจพิสูจน์เชิงรูปแบบอยู่แล้วก็ไม่แม่นยำ HACL* ถูกคอมไพล์เป็น C ได้ก็จริง แต่ไม่ใช่ไลบรารี C มันเขียนด้วยภาษาอื่นโดยสิ้นเชิง
      ชื่อที่ซื่อตรงกว่าน่าจะเป็นประมาณ “การคอมไพล์ส่วนย่อยของ F* เป็น Rust ที่ปลอดภัยบางส่วน พร้อมการทำให้เป็นรูปแบบบางส่วน”
    • Rust เองผ่านการตรวจพิสูจน์เชิงรูปแบบแล้วหรือยัง? เท่าที่รู้ยังไม่ใช่
    • สงสัยว่า C ที่ผ่านการตรวจพิสูจน์เชิงรูปแบบ คืออะไรกันแน่ และทำไมถึงไม่มีมากกว่านี้
    • สงสัยว่าความแตกต่างหลักคืออะไร สามารถบังคับให้ปฏิบัติตามได้ด้วยแฟล็กคอมไพเลอร์ไหม?
  • ในปี 2002 นักวิจัยได้ตีพิมพ์บทความเกี่ยวกับ Cyclone ซึ่งเป็นภาษาถิ่นของ C ที่ปลอดภัย และระหว่างพอร์ตโค้ด C ไปเป็น Cyclone ด้วยมือ ก็พบข้อบกพร่องด้านความปลอดภัยในโค้ด C ที่มีอยู่
    การแปลง C แบบทำมือหรืออัตโนมัติทำนองนี้มีศักยภาพไม่เพียงในการเพิ่มการนำภาษาที่ปลอดภัยกว่าไปใช้ แต่ยังช่วยเผยบั๊กที่มีอยู่เดิมด้วย
    [1] https://www.researchgate.net/profile/James-Cheney-2/publicat...

    • Cyclone ไม่ได้รับการสนับสนุนแล้ว โปรเจกต์วิจัยหลักก็จบไปแล้ว และนักพัฒนาก็ย้ายไปทำอย่างอื่น
      แนวคิดหลายอย่างของ Cyclone เข้าไปอยู่ใน Rust และโค้ดสามารถทำให้รันได้ถ้าพยายาม แต่บนแพลตฟอร์ม 64 บิตสมัยใหม่จะยังบิลด์ไม่ได้ทันที
      http://cyclone.thelanguage.org
  • ผมเคยใช้ C2Rust เป็นขั้นตอนแรกในการพอร์ตโปรเจกต์บางตัว รวมถึงโปรเจกต์ C ไปเป็น Rust และได้ข้อสรุปบางอย่าง

    1. เมื่อย้ายโปรแกรม C ไปเป็น Rust แม้จะมี unsafe อยู่ด้วย ข้อจำกัดที่เข้มแข็งของ Rust เช่น การตรวจขอบเขตและซิกเนเจอร์ที่เข้มงวด มักทำให้บั๊กปรากฏเร็วขึ้น
    2. ผมคิดว่าการแปลง C→Rust อัตโนมัติไม่สามารถแก้ได้อย่างสมบูรณ์ การออกแบบของโปรแกรม C แตกต่างจาก Rust โดยพื้นฐาน และถ้าจะทำให้ปลอดภัยจำเป็นต้องออกแบบใหม่ค่อนข้างมาก
    3. ในบางกรณี การย้ายจาก C ไปเป็น Rust พร้อมรักษาความหมายที่ถูกต้องไว้อาจเป็นไปไม่ได้ เพราะ ความไม่ปลอดภัย อาจฝังอยู่ในการออกแบบเอง
      ถึงอย่างนั้น เครื่องมือก็จำเป็นต่อการพอร์ต และยิ่งเครื่องมือพัฒนาขึ้น กระบวนการก็จะยิ่งราบรื่นขึ้น
    • การแปลงอัตโนมัติเป็น “Rust ที่เร็วและปลอดภัย” นั้นยาก แต่การแปลงอัตโนมัติเป็น Rust ที่ปลอดภัย ทั่วไปนั้นง่ายกว่ามาก
      แค่แสดงหน่วยความจำเป็นอาร์เรย์ และมองพอยน์เตอร์เป็นดัชนีของอาร์เรย์นั้น ก็จะสามารถแสดงพฤติกรรมของ C อย่าง pointer arithmetic ที่ไม่มีการตรวจสอบหรือ union ได้โดยไม่ต้องสู้กับตัวตรวจสอบการยืม และยังรักษาความหมายไว้ได้ด้วย ใน C→Java ก็ใช้เทคนิคคล้ายกันมานานแล้ว
      แน่นอนว่ามูลค่าของการแปลงแบบนี้ค่อนข้างคลุมเครือ โดยพื้นฐานมันคล้ายกับการคอมไพล์ C เป็น wasm แต่ช้ากว่า และแม้โค้ดผลลัพธ์จะ “ปลอดภัย” ในเชิงเทคนิค ปัญหาอย่างบัฟเฟอร์โอเวอร์โฟลว์ที่ทำให้เกิดสถานะผิดพลาด หรือพอยน์เตอร์ค้างที่ทำให้เข้าถึงข้อมูลในบริบทที่ไม่ควรอนุญาต ก็ยังคงอยู่
    • ผมเห็นด้วยในหลักการกับคำพูดที่ว่า “ความไม่ปลอดภัยอาจฝังอยู่ในการออกแบบ” และจากประสบการณ์ของผมก็รู้สึกเช่นนั้นอย่างแรง แต่ถ้ามี ตัวอย่างง่าย ๆ เพื่อทำให้การถกเถียงเป็นรูปธรรมก็น่าจะดี
  • ผมเป็นผู้เขียนเอง คิดว่าน่าจะเป็นประโยชน์ถ้าสรุปประเด็นบางอย่างจากหลายเธรด

    1. นี่คือ บทความวิชาการ ที่อัปโหลดลง arxiv ไม่ใช่การประกาศผลิตภัณฑ์ใหม่ที่อ้างว่าแก้ปัญหา C→Rust ได้แล้ว เราส่งไปยังงานประชุม PL และกลุ่มผู้ฟังกับความคาดหวังก็ต่างจากการนำเสนอในงานโอเพนซอร์สอย่าง FOSDEM
    2. แนวทางนั้นเรียบง่าย เราเริ่มจากข้อจำกัดว่าจะแปล C เป็น Rust ที่ปลอดภัย แล้วพิจารณาว่าจำเป็นต้องมีซับเซตเล็ก ๆ ของ C ที่ทำงานได้ดี, การอนุมานการแบ่ง slice, การแปลที่อาจรายงานข้อผิดพลาดได้, และโปรแกรมที่อาจถูกหยุดกลางทางได้ เป็นต้น เราประเมินกับเป้าหมายที่เรามีคือ C ที่ฝังอยู่ใน F* และแสดงให้เห็นว่าภายใต้ข้อจำกัดนี้ มันขยายไปใช้กับไลบรารี C ขนาดใหญ่ที่ใช้ในซอฟต์แวร์กระแสหลักอย่าง Firefox, Python ฯลฯ ได้ค่อนข้างดี เราไม่ได้อ้างว่าสามารถเขียน Firefox ใหม่เป็น Rust โดยอัตโนมัติได้
    3. งานวิจัยโดยปกติก็ดำเนินไปแบบนี้ เราเห็นว่ามีจุดที่น่าสนใจในพื้นที่การออกแบบ และแม้จะไม่ได้อ้างว่าแก้ได้ทุกปัญหา แต่คิดว่าเป็นไอเดียที่อาจเปิดทางให้ความก้าวหน้าเพิ่มเติมในด้านการแปล C→Rust ได้ เครื่องมือที่มีอยู่บางตัวอาจนำแนวทางนี้ไปใช้กับโค้ดที่เข้าเงื่อนไขของซับเซต และ fallback เป็น unsafe Rust สำหรับส่วนที่ไม่เข้าเงื่อนไขก็ได้
    4. นี่ยังไม่ใช่ฉบับสุดท้าย ตอนนี้เรากำลังทำ C frontend จริงด้วย libclang และกำลังสำรวจวิธีรับประกันว่า Rust ที่สร้างออกมาจะไม่ก่อให้เกิดการเข้าถึงนอกขอบเขต เช่น กำลังคิดถึงแนวทางส่ง verification conditions ออกไปให้ Z3 ถ้าผู้รีวิวเห็นว่าต้องทำงานเพิ่ม ก็ปรับปรุงแล้วส่งใหม่ได้ และถ้าเห็นว่าสาขานี้กำลังคึกคักและคนอื่นอาจได้ประโยชน์จากไอเดีย แล้วรับบทความนี้ไว้ก็ยิ่งดี
  • สิ่งที่สงสัยจริง ๆ คือทำไมต้องทำแบบนี้
    ถ้าเป็นเทคโนโลยีที่แปลงแอปอุตสาหกรรมจาก C เป็น Rust ได้จริง ก็น่าจะทำให้แอป C เดิม แข็งแกร่งเหมือนกันกระสุน ได้ง่ายขึ้นด้วย แค่สร้างการวิเคราะห์ที่จะใส่เข้าไปในเครื่องมือที่มีอยู่ เช่น static analyzer หรือตัวสร้างเทสต์
    ในทำนองเดียวกัน อาจสร้าง wrapper ที่ปลอดภัยขึ้นมา เพื่อให้เขียนโค้ดใหม่เป็น Rust ข้าง ๆ C ที่ตรวจสอบแล้วได้ โค้ดใหม่ได้ประโยชน์จาก Rust โค้ดเดิมก็ได้รับการยืนยันว่าปลอดภัย และอินเทอร์เฟซก็ปลอดภัยขึ้นด้วย
    ตัวแปลแบบสมบูรณ์อาจเป็นอุดมคติ เพราะระยะยาวแล้วการมีภาษาเดียวใน codebase ย่อมดีกว่า แต่สำหรับ C/C++ เดิม ความต้องการที่ใหญ่ที่สุดก็ยังเป็นการทำให้ปลอดภัยแบบกดปุ่มเดียวและมี false positive ต่ำ เช่นเดียวกับเครื่องมือคอมไพเลอร์ของ Google หรือ Mayhem ของ ForAllSecure การแก้โครงสร้างที่ไม่ดีใน C โดยอัตโนมัติก็อาจเป็นไปได้

    • โปรแกรม C บางตัวทำให้ปลอดภัยไม่ได้ ดังนั้นคำว่า “ถ้ามีเทคโนโลยีที่แปลงแอปอุตสาหกรรมเป็น Rust ได้ ก็น่าจะทำให้แอป C แข็งแกร่งขึ้นได้ง่ายกว่า” จึงไม่ถูกต้อง
      อาจเป็นเพราะมันพึ่งพา undefined behavior หรือ unspecified behavior หรือเมื่อใส่การตรวจสอบความปลอดภัยที่เหมาะสมแล้ว ขอบเขตอินพุตที่ยอมรับได้จะหดเล็กลงจนใช้ไม่ได้
      การแปลเป็นภาษาที่ปลอดภัยสามารถคงพลังการแสดงออกของอินพุตไว้ พร้อมรับประกันแบบ static ว่าจะทำงานถูกต้องตอนรันไทม์ ดังนั้นในกรณีแบบนี้จึงดีกว่าในเชิงวัตถุวิสัย
      “C ที่พิสูจน์ตัวเองในภาคสนามแล้ว” ก็แทบพูดได้ว่าไม่มีอยู่จริง อย่างที่ช่องโหว่ร้ายแรงจำนวนมากแสดงให้เห็น สิ่งที่มีจริงคือ C ที่ทำงานได้ค่อนข้างดีบ่อยพอจนดูมีประโยชน์เท่านั้น
      โค้ดเก่าถูกสมมติว่าโชคดีที่ปลอดภัย ไม่ใช่ถูกพิสูจน์แล้ว คำว่า “พิสูจน์” โดยเฉพาะในบริบทของบทความลักษณะนี้มีความหมายเฉพาะ และโค้ด C ส่วนใหญ่ท่วมท้นไม่ได้ถูกพิสูจน์ตามเกณฑ์ทางคณิตศาสตร์ที่เข้มงวด ในทางกลับกัน type system ของ Rust ได้รับการพิสูจน์ความถูกต้องทางคณิตศาสตร์แล้ว
      ตัวแปลเต็มรูปแบบขึ้นอยู่กับว่ายอมสละอะไรได้บ้าง ถ้ายอมสละประสิทธิภาพ, ขอบเขตอินพุต, ขอบเขตเอาต์พุต, ความอ่านง่ายของโค้ด ฯลฯ ก็อาจเป็นไปได้ในระดับหนึ่ง แต่พอเริ่มต้องการตัวแปลที่ sound และ complete ในทุกด้านเหล่านี้ ปัญหาก็เกิดขึ้น
  • ถ้าแปลเป็น Rust แบบตรงไปตรงมา มันจะไม่กลายเป็นส่วนที่ปลอดภัยปนกับส่วนที่เป็น unsafe หรือ? แบบนั้นงาน manual ก็น่าจะต้องตรวจแค่ความปลอดภัยของบริเวณ unsafe เหมือนกับตอนเขียน Rust ตั้งแต่ต้น
    ถ้าผลลัพธ์ 90% ไม่ใช่ unsafe ก็ดูเหมือนจะได้ประโยชน์มากทีเดียว

    • จริง ๆ แล้วใช่ มีคนเคยลองแปลง OpenJPEG ด้วย c2rust ไปเป็น unsafe Rust ระดับต่ำ
      เป็นที่รู้กันว่า OpenJPEG เกิด segfault กับเทสต์เคสบางตัว และเมื่อรันเทสต์นั้นกับเวอร์ชัน Rust ก็เกิด segfault ที่โค้ด Rust ตำแหน่งที่สอดคล้องกัน อย่างน้อยก็ถือว่าเข้ากันได้
      แต่แนวทางนั้นเป็นทางตัน ถ้าจะก้าวหน้า ตัวแปลต้องรู้จักสำนวนที่พบบ่อยใน C และยกระดับให้เป็นรูปแบบธรรมชาติของภาษาเป้าหมาย การ “คอมไพล์” เป็น Rust จะได้ Rust ที่น่ากลัว เต็มไปด้วยการเรียกฟังก์ชันจัดการพอยน์เตอร์สไตล์ C ที่ไม่ปลอดภัย
      ปัญหาการยกระดับที่ใหญ่ที่สุดส่วนใหญ่เกี่ยวข้องกับพอยน์เตอร์ ผลลัพธ์ที่มีแนวโน้มดีที่สุดของบทความนี้คือการค้นพบ วิธีเปลี่ยน pointer arithmetic ของ C ให้เป็น slice ของ Rust slice ทำสิ่งส่วนใหญ่ที่ pointer arithmetic ของ C ทำได้ และตอนนี้ก็มีคนทำให้การแปลนั้นเป็นอัตโนมัติแล้ว pointer arithmetic ที่แปลไม่ได้ควรถูกมองอย่างน่าสงสัยมาก
      การคิดว่า raw pointer ที่ชี้ไปยังอาร์เรย์ใน C มีความยาวแฝงติดอยู่ด้วยนั้นมีประโยชน์ ความยาวนั้นไม่ปรากฏในซอร์ส C แต่มีอยู่ที่ไหนสักแห่งในฐานะฟังก์ชันของสถานะโปรแกรม อาจเป็นค่าคงที่, ขนาดที่ขอจาก malloc, หรือพารามิเตอร์ของฟังก์ชันก็ได้ โดยปกติแล้วโปรแกรมเมอร์ที่ดูแลโค้ดจะหาความยาวของอาร์เรย์ได้ไม่ยากนัก
      นี่อาจเป็นปัญหาที่เหมาะกับ LLM อาจถามว่า “ดูโค้ดนี้แล้วบอกว่าความยาวของอาร์เรย์ foo คืออะไร” จากนั้นให้ตัวแปลที่ไม่ใช่ LLM ใช้คำตอบนั้นนำทางการแปลงเป็น Rust ถ้า LLM ผิด Rust ก็จะเกิด index error หรือมีอาร์เรย์ใหญ่เกินไป แต่จะไม่ไม่ปลอดภัย สำนวนการระบุขนาดอาร์เรย์ใน C มีรูปแบบค่อนข้างชัดเจน จึงน่าจะทายถูกได้เป็นส่วนใหญ่ โดยเฉพาะอย่างยิ่ง LLM ยังอ่านคอมเมนต์ได้ด้วย
    • การแปลแบบตรงไปตรงมาน่าจะสร้างโค้ด Rust ที่แทบทั้งหมดเป็น unsafe เพราะจะใช้ raw pointer แทน reference แทบทุกที่
      โค้ด C ไม่ได้เขียนโดยคำนึงถึง aliasing model และข้อจำกัดของ borrow checker ของ Rust ดังนั้นการแปลเป็น reference จึงทำได้ยาก
  • เป็นเพียงการคอมไพล์ ส่วนย่อยของ C ที่เล็กมากเท่านั้น จริง ๆ แล้วอาจเล็กจนแทบใช้งานไม่ได้ด้วยซ้ำ
    ผมไม่ค่อยคาดหวังกับแนวทางแบบนี้มากนัก มันต้องชนกับข้อจำกัดของสิ่งที่การวิเคราะห์สถิตของโค้ด C ทำได้แน่นอน แถมการเลือก Rust เป็นเป้าหมายยังทำให้ปัญหายากขึ้นโดยไม่จำเป็น โมเดล ownership ของ Rust แตกต่างจากวิธีที่โปรแกรม C จริง ๆ ทำงานมากเกินไป

    • โมเดล ownership ของ Rust ใกล้เคียงกับการแปล C มากพอแล้ว เพียงแต่มีชนิดข้อมูลที่ชัดเจนและเข้มงวดกว่า ดังนั้นตัวแปลต้องเข้าใจว่าโค้ด C ที่เขียนได้อิสระกำลังพยายามทำอะไร แล้วแมปให้เป็น idiom ของ Rust
      ตัวอย่างเช่น บัฟเฟอร์ใน C ย่อมมีความยาวอยู่แล้ว แต่ใน C ความยาวไม่ได้ผูกกับพอยน์เตอร์อย่างชัดเจน ดังนั้นตัวแปลต้องอนุมานว่าโปรแกรม C ติดตามความยาวอย่างไร แล้วแปลงเป็น slice เรื่องนี้ไม่ง่ายแม้ความยาวจะเป็นตัวแปรชัดเจน และจะยุ่งยากขึ้นอีกถ้าความยาวถูกคำนวณหรือเปลี่ยนรูปไปแสดงเป็น “พอยน์เตอร์หลังจุดสิ้นสุด”
      แพตเทิร์นใน C อย่าง bool should_free_this_pointer ก็สามารถย้ายไปเป็น enum Owned/Borrowed ของ Rust ได้ แต่ต้องอนุมานว่าการจัดสรรใดเชื่อมกับบูลีนใด และขอบเขตความปลอดภัยจริงของตัวแปรแบบยืมอยู่ตรงไหน
    • อาจดีในฐานะภาษาสำหรับอินเทอร์เฟซ มีประโยชน์กับ binding
    • สุดท้ายผู้คนก็คงโยน LLM เข้าไป แล้วบอกว่าไม่เป็นไรถ้ามัน hallucinate โค้ดจำนวนมากที่ดูเหมือนจะถูก
      ถึงอย่างนั้นก็เห็นด้วยว่าการสร้าง Rust ที่เป็น idiomatic จาก C ใด ๆ นั้นยาก พูดง่าย ๆ คือคงได้แค่ระดับ “ถูกแบบคร่าว ๆ”
  • สงสัยว่าสิ่งนี้เทียบกับ ความสามารถในการแปลง C ของ Zig อย่างไร
    Zig ดูเก่งในการสร้างสภาพแวดล้อมแบบผสมที่โค้ดใหม่เป็น Zig โค้ดเก่าเป็น C ทำการแปลงหรือทำงานร่วมกัน และยังทำหน้าที่เป็นคอมไพเลอร์ C ได้ด้วย
    น่าจะมีเหตุผลที่ดีมากที่ผู้ดูแลเคอร์เนล Linux ไม่มอง Zig เป็นตัวแทน C แทน Rust ผมไม่ได้รู้ดีพอจะเดาได้ จึงอยากให้คนที่รู้มากกว่าช่วยอธิบาย

    • Rust ไม่ได้เป็น “ตัวแทนของ C” เท่ากับเป็นเครื่องมือที่เพิ่มเข้ามาข้าง C มากกว่า เป็นเครื่องมือที่ Torvalds และคนอื่น ๆ เห็นคุณค่าและอนุญาตให้เข้ามาในเคอร์เนล และโค้ดเคอร์เนลส่วนใหญ่ก็จะยังเขียนด้วย C ต่อไป
      ผมไม่ใช่ผู้ดูแลเคอร์เนล แต่ถ้าให้เดาเหตุผลใหญ่สองข้อที่ Rust ถูกเลือกมากกว่า Zig ก็น่าจะเป็นเพราะ การรับประกันตอนคอมไพล์ ที่ภาษามอบให้นั้นดีกว่า และอัตราการนำไปใช้ก็เร็วกว่า
      บริษัทใหญ่ ๆ ในอุตสาหกรรมกำลังทำงานกันมากเพื่อให้มีโค้ดเนทีฟ Rust สำหรับ API หรือ binding Rust ที่มีการดูแลต่อเนื่อง นักพัฒนา Windows เองก็กำลังเขียนบางส่วนของเคอร์เนลใหม่ด้วย Rust เช่นกัน มีความเคลื่อนไหวนี้ต่อเนื่องมาพอสมควร และหวังว่าจะไม่หยุดลง
      ผู้ดูแลอาจรู้สึกว่า Zig ไม่ได้ให้ข้อได้เปรียบเหนือ C มากพอ หลายคนในกลุ่มนั้นยังคัดค้าน Rust อยู่ด้วยซ้ำ
    • เท่าที่เข้าใจ ผู้ดูแลเคอร์เนลส่วนใหญ่ไม่ได้ต้องการเปลี่ยน C ไปเป็นอะไรทั้งนั้น
      Zig ทำงานร่วมกับ C ได้ดีกว่า Rust มาก แต่ไม่ ปลอดภัยด้านหน่วยความจำ และยังไม่เสถียร การนำ Zig ไปใช้ในโลก C น่าจะเพิ่มขึ้นไม่น้อย แต่คงมองว่าแข่งขันกับ Rust โดยตรงได้ยาก
      ในพื้นที่ของผมไม่มีใครนำ Rust ไปใช้เลย คนสาย C++ ก็ยังอยู่กับ C++ ตอนแรกมีความสนใจ Rust อยู่บ้าง แต่ไม่ได้ตั้งหลักได้ในบริษัทไหนที่ผมรู้จักเลย อาจเป็นเหตุผลคล้ายกับที่ Go เติบโตมากในบริษัทอายุน้อย แต่เข้าไปในบริษัท Java/C# แบบดั้งเดิมได้ไม่ค่อยดีนัก เพราะแม้จะสมเหตุสมผลทางเทคนิค แต่มันเป็นโจทย์การจัดการความเปลี่ยนแปลงขนาดใหญ่
      Zig กำลังได้แรงส่งในฝั่งโปรแกรมที่ไม่ต้องการการจัดสรรหน่วยความจำแบบไดนามิก แต่นอกเหนือจากนั้นยังไม่มากนัก
    • Zig ยังไม่โตพอที่จะถูกพิจารณาในเคอร์เนล
      ยังมีการเปลี่ยนแปลงที่ทำให้ของเดิมพังอยู่เป็นประจำ ซึ่งตอนนี้เป็นเรื่องดีสำหรับ Zig แต่ไม่ดีสำหรับโค้ดเบสขนาดใหญ่และอายุยืนอย่าง Linux บั๊กของคอมไพเลอร์ก็ยังเกิดขึ้นได้
      พูดแบบนี้ในฐานะคนที่โดยรวมชอบทิศทางของ Zig
    • Zig ยังไม่ถึง 1.0 และไม่มี การรับประกันความเข้ากันได้ย้อนหลัง เลย แทบยังไม่ได้ถูกใช้ที่ไหน และแม้บางส่วนจะดูมีอนาคต แต่ก็ยังไม่ได้พิสูจน์คุณค่าของตัวเอง
    • อาจเป็นเพราะ Zig ไม่ ปลอดภัยด้านหน่วยความจำ ก็ได้
  • สงสัยว่าเครื่องมืออย่าง C2Rust จะใช้สิ่งนี้เพื่อสร้างโค้ดที่ถูกต้องตามรูปแบบพิสูจน์ได้หรือไม่
    และยังสงสัยว่าผู้เขียนทำงานด้วยมือมากแค่ไหน หรือรันอะไรบางอย่างเพื่อสร้างโค้ด Rust หรือเปล่า ถ้าเป็นอย่างนั้นก็ไม่รู้ว่าโค้ดที่สร้าง Rust อยู่ที่ไหน และก็ไม่เห็นลิงก์ไปยังซอร์สรีโพด้วย

    • ในบทความระบุว่าจะเผยแพร่ผลงานพัฒนานี้ภายใต้ ไลเซนส์โอเพนซอร์ส หลังสิ้นสุดกระบวนการรีวิว หรือโดยทั่วไปก็คือหลังบทความได้รับการตีพิมพ์อย่างเป็นทางการแล้ว
  • ถ้าไลบรารี C ทำงานได้ กล่าวคือไม่ได้พิสูจน์อย่างเป็นทางการว่าไม่มีปัญหา แต่ส่วนใหญ่ทำงานได้ดี ทำไมไม่แปลโดยใช้ unsafe Rust กันนะ
    โดยรวมแล้ว Rust ยังขาดไลบรารีอยู่มาก ผมจึงคิดว่ามีคุณค่า สุดท้ายก็ไม่ได้ต่างจากการใช้ dll/so ที่เขียนด้วย C ซึ่งอาจไม่ปลอดภัยในบางสถานการณ์มากนัก