รายงานข้อบกพร่องจากลูกค้าเรื่อง OpenSSH scp ที่ล้มเหลวได้นำไปสู่การค้นพบข้อบกพร่องใน GNU Bash ที่มีอายุหลายสิบปีและไม่ถูกสังเกตเห็นมาประมาณ 30 ปี การสืบสวนเผยให้เห็นปัญหาทางเทคนิคหลายชั้นที่เกี่ยวข้องกับ cross-compilation, พฤติกรรมของระบบไฟล์, และข้อสมมติของโค้ดเก่า
ปัญหาเริ่มต้นเมื่อลูกค้ารายงานการล้มเหลวของ scp หลังจากเปลี่ยนไปใช้ OverlayFS บนระบบ ARM 32-bit ข้อความแสดงข้อผิดพลาดแสดงให้เห็นว่า Bash ไม่สามารถระบุไดเรกทอรีการทำงานปัจจุบันได้ บางครั้งล้มเหลวด้วยข้อผิดพลาดที่เข้าใจยาก คือ Inappropriate ioctl for device สิ่งที่ดูเหมือนปัญหาความเข้ากันได้ของระบบไฟล์อย่างง่ายกลับกลายเป็นการขุดลึกผ่านชั้นซอฟต์แวร์หลายชั้น
ส่วนประกอบทางเทคนิคหลักที่เกี่ยวข้อง:
- เวอร์ชัน Bash : คอมไพล์แบบ Cross-compiled สำหรับสถาปัตยกรรม ARM
- ระบบไฟล์: OverlayFS บนระบบ 32-bit (ฟีเจอร์ xino ไม่สามารถใช้งานได้)
- ระบบ Build : Custom embedded Linux (ไม่ใช่ Yocto )
- เงื่อนไขข้อผิดพลาด: ENOTTY (Inappropriate ioctl for device), การจัดการ errno
- ไทม์ไลน์: บั๊กนี้มีอยู่เป็นเวลาประมาณ 30 ปี (ตั้งแต่ช่วงปี 1990)
Cross-Compilation เรียกใช้โค้ด Fallback โบราณ
สาเหตุหลักย้อนกลับไปที่การกำหนดค่าการสร้างของ Bash ในระหว่าง cross-compilation เมื่อทำ cross-compilation สำหรับ ARM, สคริปต์ configure ของ Bash ไม่สามารถทดสอบอย่างถูกต้องว่าฟังก์ชัน getcwd() ของระบบจัดสรรหน่วยความจำได้อย่างถูกต้องหรือไม่ เพื่อความปลอดภัย มันใช้การใช้งาน getcwd() ภายในของ Bash เอง - fallback ที่ออกแบบมาสำหรับระบบ Unix โบราณจากทศวรรษ 1990
โค้ด fallback นี้ใช้อัลกอริทึม Unix แบบคลาสสิกที่สร้างเส้นทางไดเรกทอรีใหม่ด้วยตนเองโดยการปีนขึ้นต้นไม้ระบบไฟล์ เปรียบเทียบหมายเลข inode เพื่อระบุส่วนประกอบของแต่ละไดเรกทอรี วิธีการนี้ทำงานได้อย่างน่าเชื่อถือมาหลายสิบปีบนระบบไฟล์แบบดั้งเดิม แต่ทำข้อสมมติที่ระบบไฟล์ overlay สมัยใหม่ทำลาย
การสนทนาในชุมชนเผยให้เห็นว่าปัญหา cross-compilation นี้ส่งผลกระทบต่อระบบฝังตัวจำนวนมาก ระบบสร้างหลักอย่าง Yocto มีวิธีแก้ไขปัญหาไว้ แต่สภาพแวดล้อมการสร้างที่เล็กกว่าหรือแบบกำหนดเองมักขาดการแก้ไขเหล่านี้ นักพัฒนาคนหนึ่งสังเกตว่าสคริปต์ configure ตรวจสอบเงื่อนไขที่ล้าสมัยมากมายที่ไม่ได้ใช้กับระบบจริงมาหลายสิบปี ทำให้เกิดความซับซ้อนที่ไม่จำเป็น
การวิเคราะห์สาเหตุหลัก:
- สาเหตุหลัก: สคริปต์ configure สำหรับการ cross-compilation ตั้งค่าเริ่มต้นเป็น GETCWD_BROKEN=yes
- สาเหตุรอง: ความไม่สอดคล้องของหมายเลข inode ใน OverlayFS ระหว่าง readdir() และ stat()
- สาเหตุที่สาม: บั๊กในการจัดการ errno ที่มีอายุ 30 ปีในการใช้งาน getcwd() สำรองของ Bash
- ผลกระทบต่อแพลตฟอร์ม: ส่งผลกระทบเฉพาะระบบ ARM 32 บิตที่ไม่มีการรองรับ xino
OverlayFS ทำลายข้อสมมติอายุ 30 ปี
OverlayFS ซึ่งรวมชั้นระบบไฟล์หลายชั้น จัดการหมายเลข inode แตกต่างจากระบบไฟล์แบบดั้งเดิม เมื่อแสดงรายการเนื้อหาไดเรกทอรีด้วย readdir() มันส่งคืนหมายเลข inode ดิบจากชั้นพื้นฐานโดยไม่ทำการค้นหาแบบเต็ม อย่างไรก็ตาม เมื่อรับข้อมูลไฟล์ด้วย stat() มันให้หมายเลข inode ที่เสถียรและไม่ซ้ำกันผ่านการค้นหาแบบเต็ม
ตัวเลือกการออกแบบนี้ให้ความสำคัญกับประสิทธิภาพสำหรับการแสดงรายการไดเรกทอรีในขณะที่รักษาความแม่นยำสำหรับการดำเนินการไฟล์แต่ละไฟล์ เครื่องมืออย่าง find และ du ทำงานได้อย่างถูกต้อง แต่โค้ด fallback โบราณของ Bash คาดหวังให้หมายเลข inode จากการดำเนินการทั้งสองตรงกัน - ข้อสมมติที่ OverlayFS ทำลาย
ปัญหานี้ส่งผลกระทบต่อระบบ 32-bit โดยเฉพาะ ซึ่ง OverlayFS ไม่สามารถใช้ฟีเจอร์ xino เพื่อให้หมายเลข inode ที่สอดคล้องกันได้ บนระบบ 64-bit พื้นที่พิเศษในฟิลด์ inode ช่วยให้สามารถเข้ารหัสข้อมูลเพิ่มเติมเพื่อป้องกันความขัดแย้ง แต่ระบบ 32-bit ขาดความสามารถนี้
ข้อบกพร่อง errno อายุ 30 ปี
บางทีที่น่าประหลาดใจที่สุด การสืบสวนเผยให้เห็นข้อบกพร่องที่ละเอียดอ่อนแต่มีมายาวนานในการจัดการข้อผิดพลาดของ Bash ฟังก์ชัน readdir() ส่งคืน NULL ทั้งเมื่อถึงจุดสิ้นสุดของไดเรกทอรีและเมื่อพบข้อผิดพลาด เพื่อแยกแยะระหว่างกรณีเหล่านี้ โปรแกรมต้องตั้งค่า errno เป็นศูนย์ก่อนเรียก readdir()
การใช้งาน getcwd() fallback ของ Bash ลืมขั้นตอนสำคัญนี้มาสามสิบปี เมื่อ readdir() ไม่พบรายการไดเรกทอรีที่ตรงกัน (กรณีปกติกับ OverlayFS), Bash ตีความผิดว่านี่เป็นข้อผิดพลาดและส่งคืนค่า errno ใดก็ตามที่เหลืออยู่จากการเรียกระบบก่อนหน้า นี่อธิบายข้อความ Inappropriate ioctl for device ที่ทำให้เข้าใจผิด
99% ของเวลา คุณไม่จำเป็นต้องตั้งค่า errno = 0 ก่อนทำการเรียก คุณตรวจสอบการส่งคืนที่ไม่ใช่ศูนย์ และจึงดูที่ errno แต่บางครั้งคุณจำเป็นต้องตั้งค่า errno = 0 เพราะในกรณีนี้ readdir() ส่งคืน NULL ทั้งในกรณีข้อผิดพลาดและ EOF
ข้อบกพร่องไม่ถูกสังเกตเห็นเพราะระบบส่วนใหญ่ใช้ฟังก์ชัน getcwd() ของไลบรารีมาตรฐานแทนการใช้งาน fallback ของ Bash เฉพาะการรวมกันเฉพาะของการกำหนดค่า cross-compilation และ OverlayFS เท่านั้นที่เปิดเผยการมองข้ามที่มีอายุหลายสิบปีนี้
วิธีแก้ไขและแนวทางแก้ปัญหา:
- การแก้ไขเร่งด่วน: เขียนทับ bash_cv_getcwd_malloc=yes ในการกำหนดค่าการ build
- การแก้ไขระยะยาว: รายงานบั๊ก errno ให้กับโครงการ GNU Bash
- แนวปฏิบัติในอุตสาหกรรม: ระบบ build ของ Yocto มีการเขียนทับที่จำเป็นอยู่แล้ว
- ทางเลือกอื่น: ใช้ getcwd() ของ libc สมัยใหม่แทนการใช้งาน fallback implementation ของ Bash
บทเรียนสำหรับการพัฒนาซอฟต์แวร์สมัยใหม่
การตามหาข้อบกพร่องนี้แสดงให้เห็นว่าโค้ดเก่าสามารถสร้างปัญหาที่ไม่คาดคิดในสภาพแวดล้อมสมัยใหม่ได้อย่างไร ปัญหานี้ต้องการให้ปัจจัยสี่ประการแยกกัน: การกำหนดค่า cross-compilation ผิด, การใช้งาน OverlayFS, สถาปัตยกรรม 32-bit, และการดำเนินการไดเรกทอรีเฉพาะ
นักพัฒนาได้รายงานข้อบกพร่องการจัดการ errno ไปยังโครงการ GNU Bash และใช้การแก้ไขระบบสร้างเพื่อป้องกันปัญหา cross-compilation อย่างไรก็ตาม การสืบสวนเน้นความกังวลที่กว้างขึ้นเกี่ยวกับการรักษาข้อสมมติความเข้ากันได้ในระบบนิเวศซอฟต์แวร์ที่พัฒนา
ระบบไฟล์ overlay สมัยใหม่แสดงถึงการเปลี่ยนแปลงพื้นฐานในวิธีการทำงานของระบบจัดเก็บข้อมูล อาจส่งผลกระทบต่อแอปพลิเคชันเก่าอื่นๆ ที่ทำข้อสมมติคล้ายกันเกี่ยวกับความสอดคล้องของหมายเลข inode เมื่อการใช้คอนเทนเนอร์และระบบไฟล์ overlay กลายเป็นที่แพร่หลายมากขึ้น ปัญหาความเข้ากันได้ที่คล้ายกันอาจปรากฏในส่วนประกอบซอฟต์แวร์ที่เสถียรมายาวนานอื่นๆ
อ้างอิง: Deep Down the Rabbit Hole: Bash, OverlayFS, and a 30-Year-Old Surprise