- การตัด Rust ออกเพียงเพราะมี
unsafeและใช้ เกณฑ์ที่มองว่าเฉพาะ Fil-C เท่านั้นที่ปลอดภัย ทำให้มองข้ามขอบเขตการใช้งานจริงของซอฟต์แวร์และการประนีประนอมทางเทคนิค - Fil-C เปลี่ยนการเข้าถึงหน่วยความจำที่ผิดพลาดของ C/C++ ให้กลายเป็นแพนิก แต่มีต้นทุนตามมา เช่น ABI ไม่เข้ากัน ประสิทธิภาพลดลงหลายเท่าในบางสถานการณ์ และการนำ GC มาใช้
- ในโค้ด Rust ของ Android ประมาณ 5 ล้านบรรทัด พบช่องโหว่ด้านความปลอดภัยของหน่วยความจำที่อาจเกิดขึ้น 1 รายการและแก้ไขก่อนปล่อย คิดเป็นประมาณ 0.2 รายการต่อหนึ่งล้านบรรทัด ซึ่งต่ำกว่าข้อมูลเดิมของ C/C++ ที่ประมาณ 1,000 รายการ มากกว่า 1,000 เท่า
- ไม่จำเป็นต้องเลือกเพียงอย่างเดียวระหว่างเทคโนโลยีที่ป้องกันปัญหาได้ 99.9% ในทุกโปรแกรม กับเทคโนโลยีที่ป้องกันได้ 100% ใน 90% ของโปรแกรม และสำหรับซอฟต์แวร์ที่ยอมรับ ข้อจำกัดของ Fil-C ได้ยาก ทางเลือกอย่าง Rust ก็เหมาะสม
- ความปลอดภัยของหน่วยความจำต้องพิจารณา ประสิทธิภาพ·ABI·GC·การป้องกัน data race ร่วมกัน และหากวิจารณ์ว่า Rust ยังไม่เพียงพอ ก็ควรใช้เกณฑ์เดียวกันอย่างน้อยกับ C/C++ ทั่วไปและ Zig ที่ไม่ใช่ Fil-C
โมเดลความรับผิดชอบของ Rust และภาษาระบบแบบเดิม
- การถกเถียงเรื่องความปลอดภัยของหน่วยความจำในภาษาสำหรับ system programming ที่ไม่ใช้ GC ส่วนใหญ่ดำเนินอยู่รอบความแตกต่างของ โมเดลความรับผิดชอบระหว่าง Rust กับ C/C++/Zig
- Rust พยายามป้องกันไม่ให้โปรแกรมที่อาจก่อปัญหาความปลอดภัยของหน่วยความจำคอมไพล์ผ่าน แม้ต้องแลกด้วยการปฏิเสธบางโปรแกรมที่จริง ๆ แล้วอาจปลอดภัย
unsafeเป็นทางออกที่อนุญาตให้ข้ามการรับประกันบางอย่าง เช่น การ dereference พอยน์เตอร์ดิบ- ภาษาตระกูล C มอบความรับผิดชอบด้านการรับประกันความปลอดภัยของหน่วยความจำส่วนใหญ่ให้โปรแกรมเมอร์
- แม้ระดับการสนับสนุนของแต่ละภาษาจะแตกต่างกัน เช่น RAII และ smart pointer ของ C++ หรือ
deferของ Zig แต่โดยหลักการแล้วไม่ได้ปิดกั้นการเข้าถึงหน่วยความจำที่ผิดพลาดโดยตรง
ตัวเลือกที่ Fil-C เพิ่มเข้ามา
- Fil-C นำเสนอแนวทางใหม่ในการรันโค้ด C และ C++ ให้ปลอดภัยด้านหน่วยความจำ
- หากเกิดการเข้าถึงหน่วยความจำที่ผิดพลาด เช่น เข้าถึงนอกขอบเขตหรือใช้งานหลังจาก free แล้ว จะทำให้เกิด แพนิก
- ผสาน GC เข้ากับ InvisiCaps ที่ติดตามหน่วยความจำซึ่งพอยน์เตอร์สามารถเข้าถึงได้
- ใน Zig ก็มีการเสนอ โหมดคอมไพล์ใหม่ ที่ได้แรงบันดาลใจจาก Fil-C
- หากโปรเจกต์ C/C++ ยอดนิยมบางส่วนให้บริการรีลีสที่คอมไพล์ด้วย Fil-C ก็อาจเพิ่มทางเลือกในการลดช่องโหว่ด้านความปลอดภัยของหน่วยความจำได้
เกณฑ์ที่มองว่า Rust ไม่ปลอดภัย
- ผู้พัฒนา Fil-C เคยประเมินบน Twitter ว่า Rust ไม่ใช่ภาษาที่ปลอดภัยด้านหน่วยความจำ เพราะ
unsafeสามารถข้ามการรับประกันบางอย่างได้ - Andrew Kelley ผู้พัฒนา Zig ก็ใช้ชื่อประเด็นที่เกี่ยวข้องเรียกโหมดที่ได้แรงบันดาลใจจาก Fil-C ว่าเป็นโหมดคอมไพล์ที่ “ปลอดภัยด้านหน่วยความจำจริง ๆ ต่างจาก Rust”
- การถกเถียงบางส่วนเรียกร้องว่า หากผู้ใช้ Rust ให้ความสำคัญกับความปลอดภัยของหน่วยความจำอย่างแท้จริง ก็ควรเลิกใช้ Rust แล้วไปโปรโมต Fil-C ที่ปลอดภัยกว่า
- เกณฑ์นี้เปรียบเทียบ Rust กับ Fil-C โดยตัด ต้นทุนในโลกจริงของ Fil-C ออกไป และมีท่าทีคล้ายกับคำวิจารณ์เรื่องความคลั่งไคล้ที่ชุมชน Rust มักได้รับ
ข้อจำกัดในการนำ Fil-C ไปใช้
- Fil-C ไม่ใช่ ตัวแทนแบบ drop-in ที่ไร้ต้นทุน
- ABI ไม่เข้ากันกับโปรแกรมที่คอมไพล์โดยไม่ใช้ Fil-C
- ในบางสถานการณ์อาจช้าลงหลายเท่า
- นำ GC เข้ามาใช้
- สำหรับโปรแกรมอย่างยูทิลิตีง่าย ๆ ที่แทบไม่รู้สึกถึงประสิทธิภาพที่ลดลง หรือไม่จำเป็นต้องลิงก์แบบไดนามิก ข้อจำกัดเหล่านี้อาจไม่ใช่ปัจจัยชี้ขาด
- ในทางกลับกัน มีโปรเจกต์ยอดนิยมจำนวนมากที่ไม่สามารถยอมรับ GC และ ABI ที่ไม่เข้ากันได้ และโปรแกรมที่ใช้ Fil-C ในรูปแบบปัจจุบันได้ยากก็มัก เหมาะกับ Rust
ข้อมูลช่องโหว่ของโค้ด Rust ในโลกจริง
- ข้อมูลสำหรับประเมินความปลอดภัยเชิงปฏิบัติของ Rust ยังมีไม่มาก แต่ก็ยังไม่พบช่องโหว่ด้านความปลอดภัยของหน่วยความจำที่ใช้โจมตีได้จำนวนมากในซอฟต์แวร์ Rust
- ใน โค้ด Rust ของ Android มากกว่า 5 ล้านบรรทัด พบช่องโหว่ด้านความปลอดภัยของหน่วยความจำที่อาจเกิดขึ้น 1 รายการ และแก้ไขก่อนปล่อย
- ความหนาแน่นของช่องโหว่ที่ประเมินได้คือ 0.2 รายการต่อหนึ่งล้านบรรทัด
- ข้อมูล C/C++ ในอดีตของ Android อยู่ที่ประมาณ 1,000 รายการต่อหนึ่งล้านบรรทัด
- ความหนาแน่นของโค้ด Rust ถูกติดตามว่า ต่ำกว่า C/C++ มากกว่า 1,000 เท่า
- ตัวเลขอาจแตกต่างกันไปตามแต่ละโปรเจกต์ แต่เป็นหลักฐานว่าในสภาพแวดล้อมจริง Rust ลดความเสี่ยงในการนำปัญหาความปลอดภัยของหน่วยความจำเข้ามาได้อย่างมาก
เหตุผลที่ไม่จำเป็นต้องเลือกเพียงอย่างเดียว
- ตัวเลือกสมมติระหว่างเทคโนโลยีที่ป้องกันปัญหาได้ 99.9% ในทุกโปรแกรม กับเทคโนโลยีที่ป้องกันปัญหาได้ 100% ใน 90% ของโปรแกรม แสดงให้เห็นว่า ทั้งขอบเขตการใช้งานและระดับการป้องกัน ล้วนสำคัญ
- แม้จะไม่รู้สัดส่วนจริง แต่ก็ไม่จำเป็นต้องเลือกเพียงแนวทางใดแนวทางหนึ่ง
- โปรเจกต์ C/C++/Zig ที่ยอมรับการประนีประนอมได้สามารถให้บริการไบนารี Fil-C ได้
- ซอฟต์แวร์ที่ใช้ Fil-C ไม่ได้สามารถเขียนด้วยภาษาที่กำจัดความเสี่ยงของช่องโหว่ด้านความปลอดภัยของหน่วยความจำได้ทั้งหมดหรือเกือบทั้งหมด
เหตุผลที่เลือก Rust แม้ใช้ GC ได้
- การใช้ Rust ยังคงสมเหตุสมผล แม้มีตัวเลือกที่ใช้ GC อย่าง Go หรือ Fil-C
- โปรแกรมที่เขียนด้วยภาษาแบบ GC ได้มักไม่จำเป็นต้องใช้
unsafeและโปรแกรมที่จำเป็นต้องใช้unsafeก็มักใช้ GC ไม่ได้ - อาจประเมินว่าการรับประกันและฟีเจอร์อื่นของภาษา เช่น การป้องกัน data race สำคัญกว่าความเสี่ยงด้านความปลอดภัยของหน่วยความจำเพียงเล็กน้อย
- Fil-C เปลี่ยนช่องโหว่ด้านความปลอดภัยของหน่วยความจำใน C/C++ เดิมให้กลายเป็นการแครช
- แม้จะดีกว่าช่องโหว่ด้านความปลอดภัย แต่หากความหนาแน่นในอดีตประมาณ 1,000 รายการต่อหนึ่งล้านบรรทัดยังคงอยู่ ก็ยังเหลือการแครชจำนวนมากที่ต้องแก้ไข
- ในอดีตเคยมีช่องโหว่ด้านความปลอดภัยที่อาศัยความสามารถของผู้โจมตีในการทำให้โปรแกรมแครชได้
เกณฑ์ความปลอดภัยของหน่วยความจำที่สอดคล้องกัน
- หากไม่สามารถยอมรับแม้แต่ 0.2 รายการต่อหนึ่งล้านบรรทัดของ Rust ได้ ก็ควรใช้คำวิจารณ์ที่เท่ากันหรือเข้มงวดกว่าเดิมกับ C/C++ ทั่วไปและ Zig ที่ไม่ใช่ Fil-C ด้วย
- ท่าทีที่ยอมรับทางเลือกที่มีความปลอดภัยต่ำกว่า Rust แต่ตัด Rust ออกเพียงเพราะ
unsafeไม่ได้นำแนวคิดความปลอดภัยของหน่วยความจำแบบสัมบูรณ์มาใช้อย่างสอดคล้อง
1 ความคิดเห็น
ความคิดเห็นใน Lobste.rs
คำพูดของ Andrew Kelly ที่ดูเหมือน OP จะรับไปในทางไม่พอใจ มีใจความว่า Zig สามารถคอมไพล์แม้กระทั่ง dependency C/C++ ทั้งหมดให้เป็น ไฟล์ executable ที่ปลอดภัยด้านหน่วยความจำอย่างสมบูรณ์ โดยไม่มีทางออกเลี่ยงได้ และมีต้นทุนด้านประสิทธิภาพราว 1~6 เท่า ขึ้นอยู่กับความถี่ของการติดตาม pointer
ผู้เขียนมองว่านี่เป็นการโจมตี Rust และท้ายบทความก็โจมตี Zig แต่ Fil-C กับ build mode ใหม่ของ Zig เป็นการมีส่วนร่วมเชิงบวกต่อ ecosystem และให้จุดออกแบบกับการแลกเปลี่ยนที่ต่างจาก Rust
ผมตีความว่าหมายถึง หากทำตาม data-oriented programming ที่ทีม Zig ชอบ ต้นทุนด้านประสิทธิภาพอาจลดลงจนใกล้ 1 เท่าได้
การอ่านว่ามันยั่วยุโดยไม่จำเป็นก็ไม่ใช่เรื่องแปลก และภายหลัง Andrew ก็เปลี่ยนชื่อเรื่องให้ยั่วน้อยลงแล้ว
เมื่อคำล้อเล่นเบา ๆ ว่า “ภาษาของพวกคุณไม่ปลอดภัยด้านหน่วยความจำ” ถูกโยนกลับมาในทิศทางตรงข้ามกับปกติ ปริมาณการตอบสนองของนักพัฒนา Rust ก็ค่อนข้างมาก
ผมเองก็ชอบ Rust มาก แต่จำเป็นต้องรับอย่างเป็นธรรม
ยิ่งไปกว่านั้น C ที่ผ่านการ formal verification ของ seL4 ก็ปลอดภัยยิ่งขึ้นอีกขั้น
Zig มีข้อดีตรงที่เมื่อการจัดสรรหน่วยความจำล้มเหลวก็ทำให้จบการทำงานอย่างถูกต้องได้ง่าย และคอมไพล์ได้เร็ว
ผมไม่รู้ว่า Fil-C ปลอดภัยด้านหน่วยความจำกว่า Rust หรือไม่ แต่สำหรับการใช้งานของผม garbage collector และความเข้ากันไม่ได้กับ C ABI เป็นอุปสรรคชี้ขาด
หากทำเป็น single-thread ก็เลี่ยง race condition ได้ และถ้าใส่ garbage collector ก็ได้ memory safety แต่สิ่งที่ดีของ Rust คือให้ทั้งสองอย่างโดยไม่ต้องยอมแลกสองข้อนั้น
เนื่องจากให้ความสำคัญกับ memory safety มาก Fil-C และ การใช้งาน Fil-C ABI ของ Zig จึงเป็นตัวเลือกที่ชัดเจน
โปรเจกต์ Rust ที่มี dependency เป็น C จะทำให้การรับประกันความปลอดภัยอ่อนลง และแม้จะใช้แต่ Rust ล้วนก็ทำได้ แต่ไม่สะดวก
Rust เองก็ควร implement Fil-C ABI เพื่อให้สามารถ build dependency C อย่างปลอดภัยและ link เข้ากับ Rust ได้ ผมไม่เข้าใจว่าทำไมเรื่องนี้ถึงเป็นประเด็นขัดแย้ง
หากจะ implement ความสามารถคล้ายกัน ก็อยากให้ใช้เป็นเครื่องมือช่วยปรับปรุงโค้ด FFI เฉพาะใน debug build เท่านั้น
วิธีที่มี garbage collector และตรวจทุก operation ตอน runtime ไม่เหมาะกับทุกการใช้งาน
ก่อนจะปัด Fil-C ว่าเป็นแค่ของสำหรับ utility ง่าย ๆ ควรดู งานนำเสนอ Fil-C จากงานประชุม Software Should Work
ผู้พูดนำเสนอด้วยแล็ปท็อป Linux ที่ประกอบจาก Fil-C ทั้ง userspace และรวมถึง OpenOffice Impress
มันอาจไม่เหมาะกับโปรแกรม C/C++ บางส่วน แต่ดูไม่ใช่ เทคโนโลยีของเล่น เท่าที่ผู้เขียนคิด
มาจากสาย Python จึงมองว่า memory safety เป็นเงื่อนไขพื้นฐานอยู่แล้ว และเหตุผลที่เลือก Rust มีสามข้อ
หนึ่ง ระบบ type ที่ทรงพลังช่วยให้มั่นใจความถูกต้องได้ตั้งแต่ compile time และ memory safety ที่ได้จาก
#![forbid(unsafe_code)]กับการ audit dependency ด้วย cargo-geiger เป็นการแสดงออกที่น่าสนใจน้อยที่สุดในบรรดานั้นสอง มี ecosystem ที่ช่วยให้เขียนโค้ดอย่างปลอดภัยครั้งเดียว แล้วแชร์ข้ามหลายภาษาและ execution environment ได้ง่าย
สาม มี syntax sugar ที่ช่วยให้เขียนโค้ดระดับสูงได้สะดวก เช่น
try!(x)ในสมัยนั้นZig และ Fil-C ดูเหมือนยังไม่ตอบโจทย์ ความสามารถให้ compiler จับ logical error ด้วยการ encode invariant ลงใน type system ผ่าน typestate pattern หรือ newtype เป็นต้น
ความไม่เข้ากันของ ABI ใน Fil-C ก็เป็นปัญหาเมื่อเขียนโมดูลที่คอมไพล์อย่างปลอดภัยสำหรับ runtime เดิมอย่าง CPython บน shared web hosting
comptimeมีพลังในการแสดงออกมากกว่า Rustมีการแลกเปลี่ยนเรื่องเวลา compile ความยืดยาว และข้อเท็จจริงที่ว่า ecosystem ส่วนใหญ่ไม่ได้พยายามไปถึงระดับนี้ แต่ทำได้จริงและค่อนข้างสนุก
สงสัยว่าทำไมเทคโนโลยีอย่าง Fil-C ถึง ไม่เกิดขึ้นเมื่อ 20 ปีก่อน
แต่ไม่มีใครอยากจ่ายต้นทุนด้วย CPU หรือหน่วยความจำ กล่าวคือด้วยค่าใช้จ่ายจริง
ช่วงปี 2004~2018 มีไอเดียอยู่แล้ว แต่มองว่าแนวคิด C ที่ปลอดภัยด้านหน่วยความจำเป็นเรื่องโง่ ช่วงปี 2018~2023 เปลี่ยนความคิดแล้ว แต่ยังหาวิธีให้เข้ากันได้แบบสุดขั้วไม่ได้
Fil-C ระยะแรกในปี 2023~2024 มีความเข้ากันได้และประสิทธิภาพต่ำกว่านี้มาก และปลายปี 2024 ความก้าวหน้าครั้งสำคัญของ InvisiCaps ทำให้ได้ความเข้ากันได้สูงและประสิทธิภาพที่ใช้ได้อย่างในปัจจุบัน
จุดเปลี่ยนความคิดราวปี 2018 คือการสังเกตว่า C variants ที่ใช้ใน GPU เป็นรูปแบบเรียบง่ายของ C ที่ปลอดภัยด้านหน่วยความจำ
หากตีความคำกล่าวว่า “ถ้าฝั่ง Rust ให้ความสำคัญกับ memory safety จริง ก็ควรสนับสนุน Fil-C ที่ปลอดภัยกว่าและเลิกใช้ Rust” อย่างมีเจตนาดีที่สุด ก็หมายถึงตอนนี้มี Fil-C แล้ว จึงควรหยุดความพยายามเขียนโลกใหม่ด้วย Rust แล้วกลับไป C/C++ เพื่อรักษา ecosystem library ที่รวมเป็นหนึ่งเหมือนเดิม
แม้ library Rust จะใช้จาก C/C++ ได้ แต่มีนักพัฒนาที่ไม่ต้องการสิ่งนี้ ดังนั้นตรรกะคือ หากผู้ใช้ Rust ยอมรับทางออกที่ดีกว่าและยอมแพ้ ความแตกแยกก็จะหายไป
แต่ Fil-C มีการแลกเปลี่ยนที่ Rust ไม่มี นั่นคือ garbage collector และรองรับเฉพาะ x86-64 Linux
นอกจาก memory safety แล้ว Cargo และการไม่มี global namespace ก็เป็นเหตุผลสำคัญในการใช้ Rust และเป็นเรื่องน่าเสียดายที่ความแตกแยกระหว่างค่ายภาษาไปพัวพันกับสงครามวัฒนธรรมที่กว้างกว่า
แค่อยากสร้าง library ที่ทุกคนยินดีใช้เท่านั้น
ในหมู่นักพัฒนา C ก็อาจมีคนที่ไม่อยากได้ library C++ และตอนนี้ยังมี Zig กับ Odin โผล่มาอีก ต่อให้ Rust หายไป ความแตกแยกก็ยังอยู่
อยากรู้ว่า Rust สร้างความแตกแยกคนละชนิดเป็นพิเศษหรือไม่
comptimeRust encode constraint ได้มากกว่า จึงเหมาะเป็นภาษาต้นทาง