1 คะแนน โดย GN⁺ 5 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ความปลอดภัยของหน่วยความจำ ดีขึ้นอย่างมาก แต่แม้ในโค้ด Rust ระดับโปรดักชัน ปัญหาเรื่อง การจัดการขอบเขตระหว่างระบบ ก็ยังคงอยู่และอาจนำไปสู่ช่องโหว่ได้
  • โฟลว์ที่ตีความพาธเดิมซ้ำในหลาย syscall, วิธีสร้างแล้วค่อยเปลี่ยนสิทธิ์, และ การเปรียบเทียบพาธด้วยสตริง ล้วนทำให้เกิดปัญหาอย่าง TOCTOU และการเปิดเผยสิทธิ์ได้ง่าย
  • บน Unix พาธ ตัวแปรสภาพแวดล้อม และข้อมูลสตรีม ถูกส่งต่อกันในรูป ไบต์ดิบ ดังนั้นการประมวลผลที่ยึด String เป็นหลัก หรือการใช้ from_utf8_lossy, unwrap, expect อาจนำไปสู่ข้อมูลเสียหายหรือ DoS ได้
  • หากทิ้งข้อผิดพลาดไป ความล้มเหลวอาจดูเหมือนความสำเร็จ และ ความแตกต่างของพฤติกรรม เมื่อเทียบกับ GNU coreutils ก็อาจกลายเป็นปัญหาความปลอดภัยได้ทันทีในเชลล์สคริปต์และเครื่องมือแบบ privileged
  • ในการตรวจสอบครั้งนี้ ไม่พบบั๊กตระกูลความปลอดภัยของหน่วยความจำอย่าง buffer overflow, use-after-free, double-free และความเสี่ยงหลักที่ยังเหลืออยู่ก็ไม่ได้อยู่ภายใน Rust เอง แต่ไปกระจุกอยู่ที่ ขอบเขตที่เชื่อมต่อกับโลกภายนอก

ข้อจำกัดของ Rust ที่เปิดเผยจากการตรวจสอบ

  • 44 CVE ของ uutils ที่ Canonical เปิดเผย แสดงให้เห็นว่าแม้จะเป็นโค้ด Rust ระดับโปรดักชัน ก็ยังมีช่องโหว่ที่ borrow checker, clippy และ cargo audit จับไม่ได้
  • จุดศูนย์กลางของปัญหาอยู่ที่ การจัดการขอบเขตระหว่างระบบ มากกว่าความปลอดภัยของหน่วยความจำ
    • มีช่วงเวลาคั่นระหว่างพาธกับ syscall
    • ข้อมูลไบต์แบบ Unix กับสตริง UTF-8 ไม่ตรงกัน
    • มีพฤติกรรมที่ต่างจากเครื่องมือต้นฉบับ
    • มีการละเลยการจัดการข้อผิดพลาดและการจบแบบ panic!
  • รายการ CVE นี้สรุปให้เห็นอย่างกระชับว่า จุดไหนคือ ขอบเขตที่ความปลอดภัยของ Rust สิ้นสุดลง ในโค้ดระบบที่เขียนด้วย Rust

การตีความพาธสองครั้งทำให้เกิด TOCTOU

  • ถ้าตรวจสอบพาธเดียวกันใน syscall หนึ่ง แล้วค่อยไปทำงานกับพาธนั้นอีกครั้งใน syscall ถัดไป ก็มีแนวโน้มสูงที่จะนำไปสู่ช่องโหว่แบบ TOCTOU
    • ระหว่างสองการเรียก ผู้โจมตีที่มีสิทธิ์เขียนในไดเรกทอรีแม่สามารถเปลี่ยนองค์ประกอบของพาธให้เป็น symbolic link ได้
    • ในการเรียกครั้งที่สอง เคอร์เนลจะตีความพาธใหม่ตั้งแต่ต้น ทำให้งานที่มีสิทธิ์พิเศษถูกส่งไปยังเป้าหมายที่ผู้โจมตีเลือก
  • API std::fs ของ Rust ใช้การ ตีความใหม่จาก &Path เป็นค่าเริ่มต้น จึงทำให้พลาดแบบนี้ได้ง่าย
    • fs::metadata, File::create, fs::remove_file, fs::set_permissions จะตีความพาธใหม่ทุกครั้งที่เรียก
    • สำหรับเครื่องมือแบบ privileged ที่ต้องป้องกันผู้โจมตีในเครื่อง เส้นทางค่าเริ่มต้นแบบนี้จึงมีความเสี่ยง
  • ใน CVE-2026-35355 มีการโจมตีผ่านโฟลว์ที่ลบไฟล์แล้วสร้างไฟล์ใหม่ด้วยพาธเดิม
    • ใน src/uu/install/src/install.rs มี fs::remove_file(to)? ต่อด้วย File::create(to)?
    • หากระหว่างการลบกับการสร้าง to ถูกเปลี่ยนให้ชี้ไปยัง symbolic link ของเป้าหมายอย่าง /etc/shadow โปรเซสที่มีสิทธิ์สูงก็อาจเขียนทับไฟล์นั้นได้
  • การแก้ไขเปลี่ยนไปใช้ OpenOptions::create_new(true) เพื่อให้ สร้างได้เฉพาะไฟล์ใหม่เท่านั้น
    • ตามเอกสาร create_new ไม่ยอมรับทั้งไฟล์เดิมที่มีอยู่แล้วและ dangling symlink ที่ตำแหน่งปลายทาง
  • หากจำเป็นต้องทำงานกับพาธเดิมสองครั้ง การ ยึดกับ file descriptor จะปลอดภัยกว่า
    • นอกเหนือจากกรณีสร้างไฟล์ใหม่ วิธีที่เหมาะสมคือเปิดไดเรกทอรีแม่ครั้งเดียว แล้วทำงานด้วยพาธสัมพัทธ์อ้างอิงจากแฮนเดิลนั้น
    • ถ้าต้องทำงานกับพาธเดิมสองครั้ง ให้ถือว่าเป็น TOCTOU ไว้ก่อนจนกว่าจะพิสูจน์ได้ว่าไม่ใช่

อย่าแก้สิทธิ์หลังสร้าง แต่ต้องกำหนดสิทธิ์ตั้งแต่ตอนสร้าง

  • โฟลว์ที่สร้างไดเรกทอรีหรือไฟล์ด้วยสิทธิ์เริ่มต้น แล้วค่อย chmod ภายหลัง ก็ทำให้เกิด ช่วงเวลาที่เปิดเผยสิทธิ์ชั่วคราว
    • หากเขียนแบบ fs::create_dir(&path)? แล้วตามด้วย fs::set_permissions(&path, Permissions::from_mode(0o700))? ในช่วงนั้น path จะมีอยู่ด้วยสิทธิ์เริ่มต้น
    • ผู้ใช้อื่นสามารถ open() ได้ในช่วงหน้าต่างเวลานั้น และถึงจะ chmod ภายหลัง file descriptor ที่ได้ไปแล้วก็เรียกคืนไม่ได้
  • สิทธิ์ควรถูก กำหนดไปพร้อมกับการสร้าง
    • ควรใช้ OpenOptions::mode() และ DirBuilderExt::mode() เพื่อให้มันถูกสร้างมาด้วยสิทธิ์ที่ต้องการ
    • เคอร์เนลจะนำ umask มาใช้เพิ่มเติมด้วย ดังนั้นหากผลของมันสำคัญก็ต้องจัดการ umask อย่างชัดเจนเช่นกัน

การเปรียบเทียบสตริงของพาธไม่ใช่ความเป็นวัตถุเดียวกันในไฟล์ซิสเต็ม

  • การตรวจสอบ --preserve-root เวอร์ชันแรกของ chmod ใช้แค่ การเปรียบเทียบสตริง
    • recursive && preserve_root && file == Path::new("/")
    • อินพุตที่จริง ๆ แล้วชี้ไปยังรูท แต่สตริงไม่ใช่ / เช่น /../, /./, /usr/.. หรือ symbolic link ที่ชี้ไปยัง / สามารถหลบการตรวจสอบนี้ได้
  • การแก้ไขเปลี่ยนไปใช้ fs::canonicalize เพื่อตีความพาธเป็นพาธสัมบูรณ์จริงก่อนแล้วค่อยเปรียบเทียบ
    • PR ที่แก้ไข
    • canonicalize จะคืนพาธจริงหลังแก้ .., ., และ symbolic link แล้ว
  • ในกรณีของ --preserve-root วิธีนี้ใช้ได้เพราะ / ไม่มีไดเรกทอรีแม่
  • ถ้าต้องการเปรียบเทียบโดยทั่วไปว่าพาธสองอันอ้างถึงวัตถุเดียวกันในไฟล์ซิสเต็มหรือไม่ ควรเปรียบเทียบ (dev, inode) แทนสตริง
    • GNU coreutils ก็ใช้แนวทางนี้เช่นกัน
  • ใน CVE-2026-35363 rm ปฏิเสธ . และ .. แต่ยอมรับ ./ กับ ./// ทำให้สามารถลบไดเรกทอรีปัจจุบันได้
    • หากจัดการความต่างของรูปแบบอินพุตแค่ในระดับสตริง การตรวจสอบจะถูกเลี่ยงได้ง่าย

ที่ขอบเขตของ Unix ต้องให้ความสำคัญกับไบต์มากกว่าสตริง

  • String และ &str ของ Rust เป็น UTF-8 เสมอ แต่พาธ ตัวแปรสภาพแวดล้อม อาร์กิวเมนต์ และข้อมูลสตรีมของ Unix อยู่ในโลกของ ไบต์ดิบ
  • หากเลือกวิธีผิดตอนข้ามขอบเขตนี้ จะนำไปสู่บั๊กได้สองกลุ่ม
    • การแปลงแบบสูญเสียข้อมูล อย่าง from_utf8_lossy จะเปลี่ยนไบต์ที่ไม่ถูกต้องเป็น U+FFFD และทำให้ข้อมูลเสียหายแบบเงียบ ๆ
    • การแปลงแบบเข้มงวด อย่าง unwrap หรือ ? อาจปฏิเสธอินพุตหรือทำให้โปรเซสจบการทำงาน
  • CVE-2026-35346 ของ comm เป็นกรณีที่เอาต์พุตเสียหายจากการแปลงแบบสูญเสียข้อมูล
    • ใน src/uu/comm/src/comm.rs มีการแปลงไบต์อินพุต ra, rb เป็น String::from_utf8_lossy แล้ว print!
    • GNU comm จะส่งผ่านไบต์ตามเดิมแม้เป็นไฟล์ไบนารี แต่ uutils กลับเปลี่ยน UTF-8 ที่ไม่ถูกต้องเป็น U+FFFD ทำให้ เอาต์พุตเสียหาย
    • การแก้ไขคือใช้ BufWriter และ write_all เพื่อเขียน raw bytes ลง stdout โดยตรง
  • print! บังคับให้เกิด การไป-กลับผ่าน UTF-8 ผ่าน Display แต่ Write::write_all ไม่ทำเช่นนั้น
  • ในโค้ดระบบบน Unix ต้องเลือกชนิดข้อมูลให้เหมาะกับงาน
    • ใช้ Path, PathBuf สำหรับพาธไฟล์
    • ใช้ OsString สำหรับตัวแปรสภาพแวดล้อม
    • ใช้ Vec<u8> หรือ &[u8] สำหรับเนื้อหาสตรีม
  • หากอ้อมผ่าน String เพื่อความสะดวกในการฟอร์แมต ความเสียหายของข้อมูล จะซึมเข้ามาได้ง่าย

ทุก panic อาจกลายเป็นการโจมตีแบบปฏิเสธการให้บริการ

  • ใน CLI การใช้ unwrap, expect, การเข้าถึงด้วยดัชนีสไลซ์, เลขคณิตที่ไม่ตรวจสอบ, from_utf8 อาจกลายเป็น จุด DoS ได้เมื่อผู้โจมตีควบคุมอินพุตได้
    • panic! จะ unwind สแตกและหยุดโปรเซส
    • หากกำลังทำงานอยู่ใน cron job, CI pipeline หรือ shell script งานทั้งหมดอาจหยุดลง
    • ในสภาพแวดล้อมที่รันซ้ำ ๆ มันอาจก่อให้เกิด crash loop จนทำให้ทั้งระบบใช้งานไม่ได้
  • CVE-2026-35348 ของ sort --files0-from หยุดทำงานเมื่อพบ ชื่อไฟล์ที่ไม่ใช่ UTF-8 ในรายการชื่อไฟล์ที่คั่นด้วย NUL
    • ตัวพาร์เซอร์เรียก std::str::from_utf8(bytes).expect(...) กับไบต์ของแต่ละชื่อ
    • GNU sort จัดการชื่อไฟล์เป็น raw bytes เช่นเดียวกับเคอร์เนล แต่ uutils บังคับ UTF-8 ทำให้ทั้งโปรเซสหยุดทันทีเมื่อเจอพาธแรกที่ไม่ใช่ UTF-8
  • ในโค้ดที่ประมวลผลอินพุตที่เชื่อถือไม่ได้ ควรมอง unwrap, expect, การ indexing, และ as cast ว่าเป็น CVE ที่อาจเกิดขึ้นได้
    • ควรใช้ ?, get, checked_*, try_from และส่งข้อผิดพลาดจริงกลับไปยังผู้เรียก
  • มีการเสนอเกณฑ์ clippy สำหรับจับเรื่องนี้ใน CI ด้วย
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_side_effects
  • ในโค้ดทดสอบ คำเตือนเหล่านี้อาจเข้มเกินไป จึงเหมาะที่จะจำกัดไว้ภายใต้ขอบเขต cfg(test)

ถ้าทิ้งข้อผิดพลาด ความล้มเหลวอาจดูเหมือนความสำเร็จ

  • CVE บางส่วนเกิดจากการเพิกเฉยต่อข้อผิดพลาด หรือจาก โฟลว์ที่ข้อมูลข้อผิดพลาดสูญหายไป
  • chmod -R และ chown -R คืนค่าแค่ exit code ของไฟล์สุดท้าย ในการทำงานทั้งหมด
    • แม้จะล้มเหลวกับไฟล์ก่อนหน้าหลายไฟล์ แต่ถ้าไฟล์สุดท้ายสำเร็จก็อาจจบด้วย 0
    • ทำให้สคริปต์เข้าใจผิดว่างานทั้งหมดเสร็จสมบูรณ์แล้ว
  • dd เรียก Result::ok() กับผลของ set_len() เพื่อเลียนแบบพฤติกรรมของ GNU กับ /dev/null
    • เจตนาคือทิ้งข้อผิดพลาดเฉพาะในสถานการณ์จำกัด แต่โค้ดเดียวกันกลับถูกใช้กับไฟล์ทั่วไปด้วย
    • แม้ดิสก์จะเต็ม ก็อาจเหลือ ไฟล์ปลายทางที่เขียนค้างไว้เพียงครึ่งเดียว แบบเงียบ ๆ
  • หากใช้ .ok(), .unwrap_or_default(), หรือ let _ = เพื่อทิ้ง Result สาเหตุสำคัญของความล้มเหลวจะหายไป
  • ถึงจะไม่หยุดทันทีเมื่อเกิดความล้มเหลวครั้งแรก ก็ควรจำ รหัสข้อผิดพลาดที่ร้ายแรงที่สุด ไว้แล้วจบการทำงานด้วยค่านั้น
  • หากจำเป็นต้องทิ้ง Result จริง ๆ ก็ควร เขียนเหตุผลไว้ในโค้ด ว่าทำไมการเพิกเฉยต่อความล้มเหลวนั้นจึงปลอดภัย

ความเข้ากันได้กับเครื่องมือต้นฉบับอย่างแม่นยำก็เป็นฟีเจอร์ด้านความปลอดภัย

  • CVE หลายรายการไม่ได้เกิดเพราะโค้ดทำงานอันตราย แต่เกิดจาก การทำงานต่างจาก GNU
    • ในโลกจริง shell script ต่างอาศัยพฤติกรรมของ GNU ต้นฉบับอยู่ และความต่างด้านความหมายเหล่านี้อาจนำไปสู่ปัญหาความปลอดภัยได้
  • CVE-2026-35369 ของ kill -1 เป็นตัวอย่างชัดเจน
    • GNU ตีความ -1 เป็น signal 1 และยังต้องการ PID
    • uutils กลับตีความเป็น การส่งสัญญาณค่าเริ่มต้นไปยัง PID -1
    • บน Linux ค่า PID -1 หมายถึงทุกโปรเซสที่มองเห็นได้ ดังนั้นแค่พิมพ์ผิดเล็กน้อยก็อาจกลายเป็นการ kill ทั้งระบบ
  • สำหรับเครื่องมือที่นำมาเขียนใหม่ ความเข้ากันได้แบบ bug-for-bug กลายเป็นมาตรการความปลอดภัยที่ครอบคลุมถึง exit code, ข้อความผิดพลาด, edge case และความหมายของออปชัน
  • ทุกจุดที่พฤติกรรมต่างจาก GNU จะเพิ่มโอกาสที่ shell script จะ ตัดสินใจผิดพลาด
  • ตอนนี้ uutils รัน upstream GNU coreutils test suite ใน CI ร่วมด้วย
    • ดูเป็นขนาดของมาตรการป้องกันที่เหมาะสมสำหรับกันปัญหาประเภทนี้

ต้องตีความให้เสร็จก่อนข้ามขอบเขตความเชื่อถือ

  • CVE-2026-35368 เป็นกรณี local root code execution ของ chroot
  • รูปแบบปัญหาคือเรียก chroot(new_root)? แล้วค่อยไปตีความชื่อผู้ใช้ภายในรูทใหม่ที่ผู้โจมตีควบคุมได้
    • get_user_by_name(name)? ทำให้มีการอ่าน shared library จากไฟล์ซิสเต็มรูทใหม่เพื่อแปลชื่อผู้ใช้
    • หากผู้โจมตีวางไฟล์ไว้ภายใน chroot ได้ ก็อาจนำไปสู่ การรันโค้ดด้วย uid 0
  • GNU chroot จะตีความผู้ใช้ ก่อน chroot
    • การแก้ไขก็เปลี่ยนลำดับให้เป็นแบบเดียวกัน
  • เมื่อข้ามขอบเขตความเชื่อถือไปแล้ว การเรียกไลบรารีแต่ละครั้งอาจกลายเป็นการรันโค้ดของผู้โจมตีได้
  • แม้แต่การลิงก์แบบสแตติกก็ป้องกันปัญหานี้ไม่ได้
    • เพราะ get_user_by_name ผ่าน NSS และจะ dlopen โมดูล libnss_* ตอนรันไทม์

บั๊กที่ Rust ป้องกันได้จริง

  • มีบั๊กบางประเภทที่ชัดเจนว่า ไม่ถูกพบ ในการตรวจสอบครั้งนี้
    • ไม่มี buffer overflow
    • ไม่มี use-after-free
    • ไม่มี double-free
    • ไม่มี data race จากสถานะ mutable ที่ใช้ร่วมกัน
    • ไม่มี null-pointer dereference
    • ไม่มี uninitialized memory read
  • แม้เครื่องมือจะมีบั๊ก แต่ผลการตรวจสอบไม่พบบั๊กประเภทที่สามารถนำไปใช้โจมตีเพื่ออ่านหน่วยความจำตามอำเภอใจได้
  • ในช่วงไม่กี่ปีที่ผ่านมา GNU coreutils มี CVE ตระกูลความปลอดภัยของหน่วยความจำ ออกมาอย่างต่อเนื่อง
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N การเขียน NUL นอก heap buffer
    • sort อ่าน 1 ไบต์ก่อนหน้า heap buffer
    • split --line-bytes เป็น heap overwrite ใน CVE-2024-0684
    • b2sum --check อ่านหน่วยความจำที่ไม่ได้จัดสรรจาก malformed input
    • tail -f stack buffer overrun
  • เมื่อเทียบช่วงเวลาเดียวกัน งานเขียนใหม่ด้วย Rust รักษาจำนวนบั๊กหมวดนี้ไว้ที่ 0 รายการ
    • อย่างไรก็ตาม ก็มีข้อแม้ด้วยว่าการตรวจสอบนี้ไม่ได้พิสูจน์ว่าไม่มีบั๊กด้านความปลอดภัยของหน่วยความจำ เพียงแต่ยังไม่พบเท่านั้น
  • ปัญหาที่เหลืออยู่ส่วนใหญ่เกิดที่ ขอบเขตที่สัมผัสโลกภายนอก มากกว่าภายใน Rust
    • พาธ
    • ไบต์กับสตริง
    • syscall
    • ช่วงเวลาคั่นและการเปลี่ยนแปลงสถานะของไฟล์ซิสเต็ม

Rust ที่ถูกต้องก็คือ Rust ที่เป็นธรรมเนียมปฏิบัติที่ดีด้วย

  • Rust แบบที่เป็นธรรมเนียมปฏิบัติที่ดี ไม่ได้หมายถึงแค่โค้ดที่ผ่าน borrow checker และไม่มีเสียงเตือนจาก clippy
  • ความถูกต้อง เองก็ควรเป็นส่วนหนึ่งของความเป็นธรรมเนียมปฏิบัติที่ดี
    • เพราะรูปแบบโค้ดที่อยู่รอดได้ในโลกจริงคือสิ่งที่ถูกตกผลึกขึ้นจากประสบการณ์ของชุมชน
  • ระบบที่แข็งแรงไม่ควรซ่อนความสกปรกของโลกจริง แต่ควร สะท้อนมันออกมาตรง ๆ
    • file descriptor แทนพาธ
    • OsStr แทน String
    • ? แทน unwrap
    • ความเข้ากันได้แบบ bug-for-bug กับต้นฉบับ แทนความหมายที่ดูสะอาดกว่า
  • ระบบชนิดข้อมูลอาจอธิบายได้หลายอย่าง แต่ไม่สามารถครอบคลุมเงื่อนไขที่อยู่นอกการควบคุม เช่น เวลาที่ผ่านไประหว่าง syscall สองครั้ง
  • Rust ที่เป็นธรรมเนียมปฏิบัติที่ดี ควรทำให้ชนิดข้อมูล ชื่อ และ control flow ของโค้ด เผยความจริงของสภาพแวดล้อมที่มันรันอยู่
    • แม้จะสวยน้อยกว่าโค้ดบนไวท์บอร์ด แต่ก็ต้องซื่อสัตย์ต่อความจริงมากกว่า

เอกสารอ้างอิง

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

 
GN⁺ 5 시간 전
ความเห็นใน Hacker News
  • ในฐานะผู้ดูแลรักษา GNU Coreutils ฉันอ่านบทความนี้อย่างสนใจ แต่จาก Rust ที่ฉันเคยลองใช้มาบ้าง มันง่ายเกินไปที่จะสร้าง TOCTOU race ด้วย std::fs
    หวังว่าสุดท้ายแล้ว API คล้าย openat จะเข้ามาอยู่ใน standard library

    และฉันไม่เห็นด้วยกับกฎที่ว่า ให้ resolve path ก่อนค่อยเปรียบเทียบ
    โดยทั่วไปการเรียก fstat แล้วเปรียบเทียบ st_dev กับ st_ino จะดีกว่า ซึ่งในบทความก็พูดถึงจุดนี้อยู่บ้างแล้ว

    ผลข้างเคียงที่คนพูดถึงกันน้อยกว่าคือ ต้นทุนด้านประสิทธิภาพ
    ตัวอย่างจริงคือบน path ของไดเรกทอรีที่ลึกมาก cp ใช้เวลา 0.010 วินาที แต่ uu_cp ใช้เวลา 12.857 วินาที

    ในโลกจริงอาจไม่ค่อยมีใครจงใจสร้าง path แบบนี้ แต่ซอฟต์แวร์ของ GNU พยายามอย่างมากที่จะหลีกเลี่ยงข้อจำกัดที่ตั้งขึ้นตามอำเภอใจ
    https://www.gnu.org/prep/standards/standards.html#Semantics

    แล้วในบทความที่บอกว่าการ rewrite ด้วย Rust มี บั๊กด้าน memory safety เป็น 0 ในช่วงเวลาใกล้เคียงกัน อันนั้นไม่จริง :)
    https://github.com/advisories/GHSA-w9vv-q986-vj7x

    • ใช่เลย std::fs มีปัญหาแบบ lowest common denominator อยู่
      ตอน Rust 1.0 ต้องใส่อะไรสักอย่างเข้ามา และน่าเสียดายที่สภาพนั้นแข็งตัวอยู่นานมาก

      ฉันคิดว่า uutils เป็นที่ที่เหมาะสำหรับลองออกแบบ API มาแทน std::fs ที่ทำพลาดได้ยากกว่า

    • ขอบคุณที่อธิบายมุมมองฝั่งตรงข้ามได้กระชับขนาดนี้

      ฉันอยากถามว่าควรเรียนรู้อะไรจากเรื่องนี้
      สำหรับโพสต์บนอินเทอร์เน็ตถือว่าจงใจถามแบบค่อนข้างแรง เพราะถ้ามีการเปรียบเทียบชัด ๆ มันจะช่วยให้เห็นความต่างและความผิดพลาดได้ชัดขึ้น
      แน่นอนว่าไม่มีภาระใด ๆ ที่คุณต้องสละเวลาหรือพลังใจมาตอบ

      ฉันสงสัยว่าทำไมเรื่อง ความเร็ว, ประสิทธิภาพ, race condition, st_ino ถึงมักมาคู่กัน
      latency, การเขียนลง storage จริง, atomicity, ACID, ความเร็วอันจำกัดของการส่งผ่านข้อมูล ดูเหมือนสุดท้ายจะลู่เข้าหาแก่นเดียวกัน
      ระบบที่ต้องเชื่อถือได้สูงอย่างงานบัญชีดูเหมือนสุดท้ายก็ต้องลงเอยที่ ACID ส่วนระบบที่เชื่อถือได้น้อยกว่าก็อาจถูกลืมเร็วเกินไปจนทำให้รู้สึกว่าความต่างของคอมพิวเตอร์ไม่ค่อยสำคัญ

      อีกอย่าง ฉันก็สงสัยว่าในแอปพลิเคชันทั่วไป throughput สำคัญกว่า latency จริงหรือเปล่า

      แล้วแม้จะเข้าใจว่าการโฟกัสที่เลข inode มาจากประวัติของ C, Unix-like OS และ GNU coreutils
      แต่ก็อยากรู้ว่าจะมองปัญหาง่าย ๆ อย่างการทำให้ USB drive ใช้เก็บไฟล์ได้แบบใช้งานได้จริงอย่างไร
      โดยไม่เลี่ยงความซับซ้อนอย่าง libc I/O buffering, fflush, kernel buffering, multicore, time-sharing, และการรันหลายแอปพร้อมกัน

    • ฉันเป็นมือใหม่มาก ๆ เลยสงสัยว่าทำไมไม่ cd ตรง ๆ ด้วย $(yes a/ | head -n $((32 * 1024)) | tr -d '\n') แต่ต้องใช้ลูป while

      แก้ไข: เข้าใจแล้ว เป็นเพราะ -bash: cd: a/a/a/....../a/a/: File name too long

    • ไม่แน่ใจว่าคุณเคยเห็นหรือยัง แต่มีเดโมแปลง GNU utility อย่าง wget อัตโนมัติไปเป็น C++ subset ที่ปลอดภัยต่อหน่วยความจำ
      https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget

      แนวทางคือแทนที่ส่วน C ที่อันตรายด้วยส่วน C++ ที่ปลอดภัยและมีพฤติกรรมสอดคล้องกันแทบ 1:1 ดังนั้นจึงดูมีโอกาสน้อยกว่าที่จะนำบั๊กใหม่หรือความต่างของพฤติกรรมใหม่เข้ามาแบบการ rewrite

      ถ้าจัดระเบียบโค้ดต้นฉบับนิดหน่อย การแปลงสามารถทำอัตโนมัติได้เต็มรูปแบบ จึงสามารถสร้าง executable ที่ช้าลงเล็กน้อยแต่ปลอดภัยต่อหน่วยความจำจากซอร์ส C เดิมได้ในขั้นตอน build

    • อาจเป็นคำถามงี่เง่านิดหน่อย แต่สงสัยว่าฝั่ง GNU Coreutils เคยพิจารณาหรือมีแผนทำ Rust rewrite ของตัวเองหรือไม่

  • พวกเขาอาจรู้วิธีใช้ Rust แต่ไม่ได้คุ้นเคยกับ Unix API รวมถึงความหมายเชิง semantics และกับดักของมันมากพอ
    ความผิดพลาดส่วนใหญ่พวกนั้น ถ้ามองจากสายตานักพัฒนารุ่นเก่าฝั่ง GNU coreutils, BSD หรือ Solaris ถือว่าค่อนข้างระดับมือใหม่
    ปัญหาแบบนี้จำนวนมากถูกค้นพบและจัดการไปแล้วตั้งแต่หลายสิบปีก่อน และแม้ใน codebase เดิมยังมีงานแก้แบบ long tail อยู่ แต่ตอนนี้ก็เหลือเพียงปริมาณเล็กน้อยที่ยังค่อย ๆ เพิ่มเข้ามา

    • ฉันอ่าน เธรดของ Canonical นั้นแล้วอึ้งมาก
      ใจความประมาณว่า “Rust ปลอดภัยกว่า และความปลอดภัยคือสิ่งสำคัญที่สุด ดังนั้นการปล่อย rewrite ของ coreutils ทั้งชุดจึงเป็นเรื่องเร่งด่วน ถ้ามีอะไรพังก็ไม่เป็นไร ค่อยแก้ทีหลัง”

      ฉันไม่อยากรันโค้ดที่เขียนโดยคนที่คิดแบบนั้นบนเครื่องของตัวเอง
      ฉันเองก็สนับสนุน Rust แต่การที่ Rust ปลอดภัยกว่านั้นจริงได้ก็ ก็ต่อเมื่อเงื่อนไขอื่นเท่ากัน
      ซึ่งในกรณีนี้เงื่อนไขอื่นไม่ได้เท่ากันเลย

      การ rewrite ย่อมต้องมี บั๊กและช่องโหว่มากกว่าอย่างหลีกเลี่ยงไม่ได้ เมื่อเทียบกับโค้ดที่ดูแลมาหลายสิบปี ดังนั้นเหตุผลด้านความปลอดภัยอาจมีความหมายสำหรับกลยุทธ์เปลี่ยนผ่านระยะยาว แต่ไม่ใช่เหตุผลสำหรับการ rollout แบบรีบร้อน

      การลดทอนผลกระทบต่อผู้ใช้หลังปล่อยจริงว่าเป็นเรื่องเล็กน้อย หรือพูดว่า “ต้องทำแบบนี้แหละบั๊กถึงจะโผล่” หรือ “coreutils เดิมก็ไม่มีการทดสอบที่ดีเหมือนกัน” เป็นท่าทีที่ไร้ความรับผิดชอบมาก
      ผู้ใช้ไม่ใช่หนูทดลอง
      ฉันคิดว่าผู้ดูแลรักษามี ความรับผิดชอบทางศีลธรรม ที่จะไม่ทำลายความน่าเชื่อถือของระบบผู้ใช้

    • ในระดับที่ลึกกว่านั้น ดูเหมือนว่า standard library ของ Rust จะชี้นำให้นักพัฒนาใช้ API ที่ดูสะอาด แต่ อยู่ในระดับ abstraction ที่ผิด
      เช่นเอนเอียงไปทางการทำงานกับไฟล์แบบอิง path มากกว่าแบบอิง handle
      หวังว่าฉันจะคิดผิด

    • ฉันมองว่าจุดสำคัญของ Rust คือทำให้กับดักที่ใหญ่ที่สุดและตกได้ง่ายที่สุด ไม่ต้องคอยกังวลมาก

      แก่นของบทความนี้ก็ดูเหมือนจะเป็นว่า API ของ filesystem เองก็ควรทำหน้าที่แบบนั้น

    • มีคนเคยใช้คำว่า disassembler rage กับเรื่องลักษณะนี้
      หมายถึงถ้าคุณจ้องดูอะไรใกล้พอ ความผิดพลาดทุกอย่างก็ดูเหมือนฝีมือสมัครเล่นได้

      คำนี้ยังมาจากท่าทีแบบคนที่ดูแต่ disassembler แล้วด่านักพัฒนาระดับสูงว่าทำไมในฟังก์ชันที่อยู่ลึกลงไป 100 เฟรมของ call stack ถึงใช้ if แทน switch

      ตอนนี้เรากำลังเห็นเพียงไม่กี่จุดที่พวกเขาทำผิด และแทบไม่ได้เห็น โค้ดอีกหลายพันบรรทัดที่เขียนถูกต้อง รอบ ๆ มันเลย

    • การปล่อยให้ utility แบบนี้เกิด panic ขึ้นถือว่าเป็นความผิดพลาดที่ สมัครเล่นมาก แม้จะวัดตามมาตรฐานของ Rust เองก็ตาม
      ถ้าเป็น alloc error ที่จัดการไม่ได้ก็ว่าไปอย่าง แต่ expect กับ unwrap นั้นยากจะหาข้อแก้ตัวได้ เว้นแต่จะรับประกัน invariant อย่างเข้มงวดจริง ๆ ว่า code path นั้นจะไม่มีทางเกิดขึ้น

  • หนึ่งในความยากของการ rewrite โค้ดคือ โค้ดเดิมผ่านการ ค่อย ๆ กลายรูป มาเรื่อย ๆ เพื่อตอบสนองต่อปัญหาที่เผยตัวออกมาเฉพาะในสภาพแวดล้อมใช้งานจริง

    บทเรียนที่ได้จากกระบวนการนั้นค่อย ๆ ซึมเข้าไปในโค้ดอย่างเงียบ ๆ และถ้าไม่ได้ถูกบันทึกไว้ งาน เบื้องหลังที่ซ่อนอยู่ ที่ต้องทำก่อนจะไปถึงระดับเดียวกันก็จะมหาศาลมาก

    บทความต้นฉบับแสดงรายการปัญหาประเภทนั้นได้ดีมาก

    แต่ก่อนจะรีบเรียกมันว่ามือสมัครเล่น ก็ควรเห็นด้วยว่านี่เป็นหนึ่งในปรากฏการณ์ที่ “เป็นซอฟต์แวร์ที่สุด” ของวงการนี้
    ถ้าไม่ใช่ว่ามีเอกสารทางเทคนิคดีมากและมีเทสต์ครอบคลุมเคสเหล่านั้นอยู่แล้วแต่ถูกมองข้าม เรื่องแบบนี้ก็แทบจะหลีกเลี่ยงไม่ได้

    • ตัวอย่างที่ดีในบทความคือ CVE เรื่อง chroot + NSS
      กฎที่ว่า NSS เป็นแบบ dynamic และจะ dlopen ไลบรารีภายใน chroot ไม่ได้ถูกเขียนไว้เด่นชัดที่ไหนเลย

      มันใกล้เคียงกับสิ่งที่ sysadmin เรียนรู้กันจากประสบการณ์จริงตลอดกว่า 25 ปี และการ rewrite แบบ clean-room ก็มักจะกลับมาเรียนรู้สิ่งนี้ใหม่อีกครั้งในรูปของ CVE ใหม่
      ต่อให้พอร์ตโค้ดเดิมด้วย LLM ก็ไม่ต่างกันมาก
      คุณอ่าน function signature ได้ แต่สิ่งที่จำเป็นจริง ๆ คือ รอยแผลและรอยช้ำ ที่หลงเหลืออยู่ในโค้ดนั้น

    • ถ้าทำงานนี้โดยไม่อ่านแม้แต่ซอร์สต้นฉบับเพื่อหลีกเลี่ยง GPL มันก็ยิ่งยากขึ้นไปอีก

      ฉันคิดว่า uutils คงจะดีกว่านี้มาก ถ้ามันเป็น GPL และได้รับอนุญาตให้ได้แรงบันดาลใจโดยตรงจาก ซอร์ส coreutils ต้นฉบับ

    • และควรพูดให้ชัดว่าการ ไม่บันทึกบทเรียนเหล่านี้ หรืออย่างน้อยไม่บันทึกบั๊กและช่องโหว่ที่พยายามหลีกเลี่ยง ก็เป็นแนวปฏิบัติที่ไม่ดีเช่นกัน

      แน่นอนว่าการจะเขียนเอกสารครอบคลุมบั๊กทุกตัวที่ถูกหลีกเลี่ยงไว้โดยนัยจากการเขียนโค้ดที่ดีตั้งแต่ต้นนั้นทำได้ยาก แต่
      สำหรับผู้อ่านในอนาคต มันสำคัญกว่าที่จะทิ้งคำอธิบายไว้ว่า “เหตุผลที่ตรงนี้ใช้ foo แทน bar ก็เพราะถ้าใช้ bar ภายใต้เงื่อนไข ABC จะเกิด baz ที่อันตรายเนื่องจาก XYZ”
      ถึงจะดูเหมือนสิ้นเปลืองเวลาและพื้นที่เอกสารไปบ้างก็คุ้มกว่า

  • หลายจุดที่บทความนี้ชี้ให้เห็น โดยเฉพาะเมื่อเทียบกับ ซอร์ส GNU coreutils รู้สึกว่าอย่างน้อยควรถูกจับได้จาก unit test หรือการรีวิวด้วยมือ
    การ rewrite coreutils ดูเป็นความคิดที่แย่มาก
    https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
    และดูเหมือนมันถูกดำเนินไปแบบผิดทางโดยไม่ได้ยกเอาความรู้ที่สั่งสมอยู่ในซอฟต์แวร์เดิมมาให้เพียงพอ

    ถ้าจะ rewrite คุณต้องเข้าใจและเรียนรู้จากงานเดิมให้หมดก่อน
    ไม่อย่างนั้นก็จะทำผิดซ้ำเดิม และพูดตรง ๆ ว่ามันค่อนข้างน่าอาย

    เพื่อความชัดเจน ฉันชอบ Rust ใช้มันในหลายโปรเจกต์ และมันยอดเยี่ยม
    แค่ Rust ไม่ได้ช่วยคุณให้พ้นจาก วิศวกรรมที่แย่

    • ที่น่าสนใจคือ uutils ใช้ ชุดทดสอบของ GNU coreutils

      และเพิ่มเติมว่าพวกเขาก็ประกาศชัดเจนด้วยว่าจะไม่รับ contribution ที่เขียนขึ้นจากการอ่านซอร์ส GPL

    • ถ้ามาจากฝั่งที่เคยทำ unity, upstart, snap เรื่องแบบนี้ก็ถือว่าอยู่ในขอบเขตที่คาดได้

    • ดูเหมือนควรใช้คำทักทายต้อนรับนัก system programmer รุ่นใหม่ว่า
      Unix มันพังอยู่แล้ว, สุดท้ายคุณต้องเขียนวิธีอ้อมที่ทั้งน่าเกลียดและไม่ค่อยสอนอะไรด้วยตัวเอง และยังต้องทำ การทดสอบเชิงประจักษ์
      ซอฟต์แวร์ที่เชื่อถือได้และวิศวกรรมซอฟต์แวร์ที่ดีแต่เดิมก็ทำงานกันแบบนี้

  • ฉันสงสัยว่าทำไม differential fuzzing ถึงจับบั๊กพวกนี้ไม่ได้

    https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz

  • รูปแบบที่ตรวจสอบ path ด้วย syscall ครั้งหนึ่ง แล้วส่ง syscall ไปที่ path เดิมอีกครั้งเพื่อทำงาน จะนำปัญหาแบบเดิมกลับมาเสมอ
    ผู้โจมตีที่มีสิทธิ์เขียนใน parent directory สามารถสลับองค์ประกอบของ path ให้กลายเป็น symbolic link ในช่วงระหว่างนั้นได้ และ kernel จะ resolve path ใหม่ตั้งแต่ต้นในคำสั่งเรียกครั้งที่สอง ทำให้การกระทำที่มีสิทธิ์สูงไปตกกับเป้าหมายที่ผู้โจมตีเลือก

    • จริง ๆ แล้วแย่กว่านั้นอีก
      ผู้โจมตีที่เขียน parent directory ได้ยังเล่นงานด้วย hard link ได้ด้วย
      ต่อให้แตะได้เฉพาะไฟล์ธรรมดา ในทางปฏิบัติก็แทบไม่มีวิธีบรรเทาที่ดีนัก
      ดูตัวอย่างได้ที่ https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
    • อืม… อาจมีวิธีล็อกไดเรกทอรีแบบ write lock ได้ แต่ถ้ารวมปัญหาเรื่อง timeout เข้าไป มันก็คงซับซ้อนขึ้นอย่างรวดเร็ว
  • ต้นตอของบั๊ก หลายตัวดูเหมือนจะอยู่ที่ Unix API ทึบแสงเกินไป

    ตัวอย่างเช่น get_user_by_name ที่ไปโหลด shared library ภายใน root filesystem ใหม่เพื่อ resolve ชื่อผู้ใช้ แล้วทำให้ผู้โจมตีที่วางไฟล์ใน chroot ได้สามารถรันโค้ดด้วย uid 0 ได้ มันให้ความรู้สึกเกือบเหมือน กับระเบิด

    ฟังก์ชันดึงข้อมูลผู้ใช้ที่จู่ ๆ ไปโหลด shared library เพิ่มด้วย ดูเหมือนเป็นการออกแบบที่ปะปนหลาย concerns
    การค้นข้อมูลผู้ใช้กับการโหลดไลบรารีควรถูกแยกจากกันในระดับฟังก์ชัน หรืออย่างน้อยชื่อก็ควรบอกพฤติกรรมนั้นให้ชัด

    • บางส่วนอาจจริง แต่ถ้าตัดสินใจ rewrite coreutils ใหม่จากศูนย์ การ ทำความเข้าใจ POSIX API ก็คืองานหลักแบบตามตัวอักษรอยู่แล้ว

      ยิ่งไปกว่านั้น ถ้าโค้ดที่ตรวจว่ามี path ชี้ไปยัง root ของ filesystem หรือไม่ เขียนว่า file == Path::new("/") นั่นไม่ใช่ปัญหาของ API
      คนที่เขียนแบบนั้นแทบไม่มีคุณสมบัติพอจะเข้าร่วมโปรเจกต์นี้เลย

    • ตรงกันข้าม ฉันคิดว่าการใช้ ภาษาเชิงฟังก์ชันที่ปลอดภัย อาจทำให้คนเผลอคิดว่าข้อมูลที่กำลังจัดการอยู่นั้นไม่มี state ไปด้วย
      แต่ในระบบปฏิบัติการมีหลายอย่างมากที่เปลี่ยนอยู่ตลอดเวลา

      จนกว่าจะมี filesystem ที่ให้ snapshot ได้ คุณก็ต้อง ตรวจซ้ำอยู่เรื่อย ๆ

      สุดท้าย API ที่ต้องการจริง ๆ คือ API ที่เมื่อรับ input แล้วให้ผลลัพธ์เป็นสำเร็จหรือไม่ก็ล้มเหลวเท่านั้น
      ไม่ใช่ API ที่ให้หนึ่งในสามอย่างคือ success, failure, error

    • ใช่ musl libc ตัดส่วนแบบนั้นออกไปหนึ่งจุดพอดี

    • ฉันคิดว่าต้นตอจริง ๆ ไม่ใช่ความทึบของ Unix API แต่เป็นการที่ root ไป chroot เข้าไดเรกทอรีที่ตัวเองควบคุมไม่ได้ โดยไม่ได้คิดให้รอบด้าน

      อะไรก็ตามที่อยู่ในเป้าหมายของ chroot นั้นอยู่ภายใต้การควบคุมของฝั่งที่สร้างมันขึ้นมา และถ้าไม่เข้าใจจุดนี้ก็ไม่ควรใช้ chroot()

      get_user_by_name อาจให้ความรู้สึกเหมือนเป็นกับดัก แต่จริง ๆ แล้วแทบไม่มีความต่างเชิงสาระระหว่างการใช้ newroot/etc/passwd กับการใช้ newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so, newroot/bin/sh อะไรทำนองนั้น

      ดังนั้นฉันคิดว่า /usr/sbin/chroot ไม่ควรมีเหตุผลอะไรเลยที่จะต้อง lookup user ID
      toybox chroot ก็ไม่ทำแบบนั้น
      สุดท้ายบั๊กจึงไม่ได้อยู่ที่การทำบางอย่างผิดวิธี แต่คือ การไปทำสิ่งนั้นตั้งแต่แรก

    • Unix และ POSIX เต็มไปด้วยกับดักแบบแฟรกทัล ตัดตรงไหนก็เจอ

  • ต่อให้สมมติว่าคนฝั่ง Rust เขียน coreutils ใหม่โดยไม่มีประสบการณ์ Linux ฉันก็ยังไม่เข้าใจยิ่งกว่าว่า Ubuntu รับมันเข้า mainline ได้อย่างไร

    • Ubuntu ดูเหมือนมีนโยบายที่จะเปลี่ยนองค์ประกอบพื้นฐานของระบบสักอย่างหนึ่งในแทบทุกรีลีสให้กลายเป็น การทดลองที่ลวก ๆ และยังไม่เสร็จ

      ประเด็นสำคัญตรงนี้ไม่ใช่ “โอ้โห โค้ด Rust มีบั๊กด้วยเหรอ” แต่คือเรื่องนั้นต่างหาก

    • ของเดิมเป็น สัญญาอนุญาต GPL ส่วนเวอร์ชัน rewrite เป็น สัญญาอนุญาต MIT

  • ถ้าประโยคที่ว่า “บั๊กเหล่านี้มาจากโค้ด Rust ที่ถูกปล่อยใช้งานจริง และคนเขียนก็รู้ว่าตัวเองกำลังทำอะไรอยู่” เป็นความจริง
    ก็ทำให้ฉันสงสัยว่า utility ต้นฉบับไม่มี test harness เลยหรือ และฝั่ง rewrite ก็ไม่ได้เริ่มจากการสร้างสิ่งนั้นก่อนใช่ไหม

    ต่อให้มี edge case เยอะ ฉันก็ยังคิดว่าน่าจะ abstraction OS กับ FS ได้ระดับหนึ่งเพื่อเช็กอะไรอย่าง rm .// ว่าจะไม่ลบไดเรกทอรีปัจจุบันจริง ๆ ตามที่คาดหรือไม่

    นี่ดูไม่ใช่ปัญหาของการเขียนโค้ดสกปรกหรือข้อวิจารณ์ตัวภาษา แต่เหมือนเป็นทัศนคติเก่า ๆ แบบ system programming ไม่ต้องทดสอบ อีกครั้งหนึ่ง

    ในทางกลับกัน ถ้ายูทิลิตีต้นฉบับมีการทดสอบอยู่แล้วแต่ยังมีช่องโหว่มากขนาดนี้ ก็อาจแปลว่าชุดทดสอบของต้นฉบับเองมีข้อบกพร่องใหญ่พอสมควร

    • ฉันก็คิดแบบนั้น

      แต่ฉันไม่มั่นใจเท่าไรนักกับประเด็นที่ว่าคุณจะ abstraction OS และ FS ได้พอสำหรับการตรวจสอบ
      เพราะดูเหมือนผู้คนพยายามทำเรื่องนั้นมาตั้งแต่ก่อนฉันเกิด แต่ก็ยังไม่สำเร็จ

      ยกตัวอย่างแค่ว่าจะเลือกทดสอบ / ซ้ำกี่ตัวก็ยังไม่มีคำตอบชัดเจน

      ยิ่งไปกว่านั้น ถ้าสมมติว่า rm จะปฏิเสธการลบไฟล์เมื่อ 9 ไบต์แรกของไฟล์คือ important
      ถ้าคุณไม่รู้สตริงนั้นล่วงหน้า จะคิดชุดทดสอบอย่างไรให้จับพฤติกรรมแบบนั้นได้ก็น่าปวดหัว
      ยิ่งถ้าคำนั้นเป็นสตริงที่ไม่มีในพจนานุกรมก็ยิ่งยากกว่าเดิม

      ฉันแทบไม่เคยเห็นใครพูดอย่างจริงจังว่า “system programming ไม่ต้องทดสอบ”
      แต่ฉันได้ยินบ่อยกว่าว่าการทดสอบ ไม่ได้ทำหน้าที่อย่างที่คนคาดหวังเสมอไป

    • เท่าที่ฉันเข้าใจ ระหว่างการพัฒนา uutils มี การทดสอบเปรียบเทียบพฤติกรรมอย่างกว้างขวาง กับ utility ต้นฉบับ และถึงขั้นพยายามคงบั๊กเดิมไว้ด้วย

    • นี่เป็นเหตุผลหนึ่งที่ Windows ปิด symlink ไว้ตามค่าปริยาย
      ไม่ได้แก้ด้วย abstraction แต่แก้ด้วยการแทบจะเอาฟีเจอร์นั้นออกไปเลย

      ฝั่ง Unix-like ทำแบบนั้นไม่ได้ เพราะมีซอฟต์แวร์ที่พึ่งพา symlink มาหลายสิบปีมากเกินไป

      MacOS ก็มีวิธีรับมือคล้ายกัน
      ตัวอย่างเช่นบั๊ก chroot() มักไม่ใช่ปัญหาจริงในค่าตั้งต้น เพราะ MacOS บล็อก chroot() เป็นค่าเริ่มต้น
      ถ้าจะใช้ต้องปิด system integrity protection

      ปัญหาเชิงรากอยู่ที่ มุมแหลมคมของ POSIX API และทางแก้ก็ไม่ใช่การ abstraction มัน แต่ใกล้เคียงกับการ ตัดมันทิ้งไปเลย

  • ฉันโอเคกับการที่ผู้คนจะทดลองและทำพลาดแบบมือใหม่
    ก็คนเราเรียนรู้และเติบโตกันแบบนั้น

    สิ่งที่ฉันสงสัยจริง ๆ คือห่วงโซ่การตัดสินใจของ Ubuntu มันพังตรงไหนถึงได้ปล่อยของแบบนี้เข้า production

    • บางครั้งการเติบโตก็แปลว่าแค่ตัวสูงขึ้นเท่านั้น