เหตุการณ์ที่ Malloc ทำให้ JPGLoader ของ Serenity พัง หรือ: เคล็ดลับถูกรางวัลลอตเตอรี่ (2021)
(sin-ack.github.io)- ปัญหา สีเพี้ยนใน JPG ของ SerenityOS ดูเผินๆ เหมือนเป็นปัญหาลำดับอาร์กิวเมนต์ RGB/BGR แต่จริงๆ แล้วมีต้นตอมาจากการที่
JPGLoaderปล่อยให้คอมโพเนนต์ที่ต้องมีลำดับแน่นอนไปขึ้นอยู่กับลำดับการวนซ้ำของHashTable - การเพิ่ม
malloc_good_size()ในAK+LibCทำให้VectorและHashTableใช้ขนาดชังก์ที่ malloc จัดสรรจริงได้ และผลลัพธ์คือ จำนวนบัคเก็ตของ HashTable เปลี่ยนไปจนบั๊กที่ซ่อนอยู่โผล่ออกมา - โค้ดเดิมอ่านคอมโพเนนต์
Y,Cb,Crของ JPG ได้ถูกลำดับโดยบังเอิญ และเพราะผลของint_hashกับจำนวนบัคเก็ตลงตัวพอดี จึงทำให้ข้อผิดพลาดในการจัดการ สตรีม Huffman ถูกกลบไว้ - การไล่หาสาเหตุเริ่มจากข้อเท็จจริงที่ว่า
JPGLoader.cppไม่ได้เปลี่ยนล่าสุด และการ bisect กว่า 1000 คอมมิตก็ต้องรีบิลด์ทั้งระบบปฏิบัติการขนาดราว 3400 ไฟล์ หลายรอบ เพราะมีการเปลี่ยนแปลงฝั่ง AK - การแก้สุดท้ายคือทำให้การวนคอมโพเนนต์เป็นแบบ กำหนดลำดับได้แน่นอน และถ้าแก้แบบชั่วคราวด้วยการสลับลำดับอาร์กิวเมนต์สีอย่างเดียว ปัญหาเดิมก็อาจกลับมาอีกเมื่อมีการเปลี่ยนลำดับครั้งถัดไป
ปัญหาสี JPG ที่ดูเหมือนสับสนระหว่าง RGB/BGR
- เมื่อเปิดภาพ JPG ใน SerenityOS จะเกิดปัญหาที่สีแสดงผลผิดเพี้ยน
- ใน
JPGLoader.cppถ้าสลับลำดับอาร์กิวเมนต์ของคอนสตรักเตอร์Colorภาพจะดูเหมือนกลับมาเป็นปกติ- โค้ดเดิม: ส่งตามลำดับ
Y,Cb,Cr - การแก้ชั่วคราว: เปลี่ยนเป็น
Cr,Cb,Y
- โค้ดเดิม: ส่งตามลำดับ
- แต่การเปลี่ยนแปลงล่าสุดใน
JPGLoader.cppที่ไม่ใช่การ revert นั้นเกิดขึ้นก่อนหน้านั้นมากกว่าหนึ่งเดือนตาม Git และยังจำได้ว่าเมื่อ 1–2 สัปดาห์ก่อน ภาพพื้นหลัง JPG ยังแสดงผลปกติดี - จึงมีแนวโน้มสูงว่าไม่ใช่แค่ ข้อผิดพลาดเรื่องลำดับช่องสี แบบตรงไปตรงมา แต่เป็นการเปลี่ยนแปลงอื่นที่ไปเปิดเผยบั๊กเดิมที่มีอยู่แล้ว
การทำ bisect ที่ยากขึ้นเพราะ AK
- SerenityOS ใช้ AK (Agnostic Kit) ซึ่งเป็นไลบรารีมาตรฐานของตัวเอง
- AK มีบทบาทคล้าย C++ STL แต่เปลี่ยนแปลงร่วมกับโค้ดระบบปฏิบัติการในรีโพเดียวกัน
- เมื่อ AK เปลี่ยน ผลกระทบจะกว้างมาก
- ไลบรารีมาตรฐานถูก include อยู่แทบทุกส่วนของโค้ด
- เทมเพลตของ C++ ต้องมีนิยามอยู่ใน header จึงทำให้การเปลี่ยน header ของ AK กระตุ้นการคอมไพล์ใหม่ในวงกว้าง
- ทุกครั้งที่ bisect ผ่านคอมมิตที่มีการเปลี่ยน AK จำเป็นต้องบิลด์ระบบปฏิบัติการทั้งชุดใหม่
- ณ เวลาที่เขียนบทความ มีขนาดประมาณ 3400 ไฟล์
- ระหว่าง bisect ช่วง 1000 คอมมิต ผู้เขียนต้อง full build 4–5 รอบบนโน้ตบุ๊ก Sandy Bridge Mobile ปี 2011
- แม้แต่
ccacheก็ช่วยกรณีนี้ไม่ได้ และด้วยความเร็วในการเปลี่ยนแปลงของโปรเจกต์ SerenityOS ทำให้ AK มีการเปลี่ยนประมาณทุกๆ 100 คอมมิต
malloc_good_size() ที่เปิดโปงปัญหาที่ซ่อนอยู่
- หลัง bisect กว่า 1000 คอมมิต ในที่สุดก็พบว่าการเปลี่ยนแปลงที่ทำให้สี JPG พังคือฝั่ง
AK+LibCไม่ใช่JPGLoader - คอมมิตที่ทำให้ปัญหาโผล่ออกมาคือ
f89e8fb71a4893911ee5125f34bd5bbb99327d33- ชื่อ:
AK+LibC: Implement malloc_good_size() and use it for Vector/HashTable - เวลาสร้าง: 15 พฤษภาคม 2021
- ชื่อ:
- คอมมิตนี้เพิ่มการทำงานของ
malloc_good_size()ซึ่งเป็น API ของ macOS- มันคืนค่าขนาดการจัดสรรจริงสำหรับคำขอจองหน่วยความจำที่ระบุ
- ตัวอย่างเช่น ถ้าขอ 35 ไบต์แต่ภายในใช้ชังก์ 64 ไบต์ ก็จะสามารถใช้ประโยชน์จากส่วนที่เหลืออีก 29 ไบต์ได้
- หลังการเปลี่ยนนี้
Vector,HashTableและโครงสร้างอื่นๆ ใช้หน่วยความจำที่มีอยู่จริงในชังก์ malloc ได้มากขึ้น - เพราะคอมมิตก่อนหน้านั้นยังแสดงภาพ JPG ได้ปกติ จึงสรุปวงให้แคบลงได้ว่าการเปลี่ยนนี้เป็นตัวที่เผยให้เห็นปัญหาเก่าที่ซ่อนอยู่
การถอดรหัสที่พึ่งพาความจุของ HashTable โดยไม่รู้ตัว
- ตอนแรกสงสัยว่าอาจเป็นปัญหาที่
JPGLoaderหรือโค้ดชั้นบนไปพึ่งพาความจุของVectorผิดวิธีแล้วเขียนข้อมูลตรงลงไป - การเปลี่ยนที่เกี่ยวข้องกระทบทั้ง
HashTableและVectorและทั้งคู่ก็ถูกใช้ในโค้ดของJPGLoader - เมื่อสุ่มลบบรรทัดที่ใช้
kmalloc_good_size()ฝั่งHashTableแล้วคอมไพล์ใหม่ ปัญหาก็หายไป- โค้ดที่ลบคือส่วนที่ปรับความจุบัคเก็ตใหม่ให้สอดคล้องกับขนาดการจัดสรรจริง
- จากผลนี้จึงยืนยันได้ว่า การเปลี่ยนจำนวนบัคเก็ตของ HashTable ส่งผลต่อผลลัพธ์การถอดรหัส JPG
- โดยธรรมชาติแล้ว
HashTableไม่ใช่คอนเทนเนอร์ที่ใช้เหมือนสตรีมข้อมูลต่อเนื่อง ดังนั้นจึงไม่ควรพึ่งพาความจุหรือลำดับการวนซ้ำของมัน
วิธีที่คอมโพเนนต์ JPG ถูกประมวลผล
JPGLoaderเดิมจะอ่านข้อมูลคอมโพเนนต์จากส่วน Start of Frame ของไฟล์ JPG แล้วเก็บลงในโครงสร้างComponentComponentแต่ละตัวมีserial_idที่บอกตำแหน่งของมันภายในไฟล์ JPG- โดยทั่วไปลำดับคอมโพเนนต์ของ JPG ควรเป็น
Y,Cb,Cr
- โดยทั่วไปลำดับคอมโพเนนต์ของ JPG ควรเป็น
- คอมโพเนนต์เหล่านี้ถูกเก็บไว้ใน
HashTable- ต่อมาถูกใช้เพื่อตรวจสอบว่าลำดับคอมโพเนนต์ในส่วน Start of Scan ตรงกับลำดับที่คาดไว้หรือไม่
- ในขั้นตอนถอดรหัส จะมีการวนผ่านคอมโพเนนต์เหล่านี้เพื่อนำข้อมูลไปใช้กับการแปลงแมโครบล็อก
- ปัญหาคือคอมโพเนนต์ที่ลำดับมีความสำคัญถูกเก็บไว้ใน
HashTableแล้ววนด้วย iterator ปกติ
ความต่างของลำดับการวนซ้ำระหว่างคอมมิตที่พังกับคอมมิตที่ปกติ
- ในคอมมิตที่สีพัง ข้อความ debug แสดงว่ามีการวนคอมโพเนนต์ตามลำดับดังนี้
021
- ส่วนคอมมิตก่อนหน้าที่ยังปกติ ลำดับเป็นดังนี้
012
- ความต่างนี้เชื่อมโยงโดยตรงกับผลลัพธ์ที่ดูเหมือนช่องสีถูกสลับ
- ระหว่างที่ทดลองสลับลำดับคอมโพเนนต์ด้วยมือร่วมกับ CxByte ก็พบข้อผิดพลาดต่อไปนี้
Huffman stream exhausted. This could be an error!Failed to build Macroblock 3277
- ข้อผิดพลาดนี้เผยให้เห็นว่าการถอดรหัส JPG ไวต่อ ลำดับของสตรีม และยืนยันว่าลำดับการวนคอมโพเนนต์คือสาเหตุหลัก
ลำดับของ HashTable ที่บังเอิญลงตัวพอดี
- สาเหตุแท้จริงคือการเก็บอ็อบเจ็กต์ที่ต้องมีลำดับแน่นอนไว้ใน
HashTableแล้ววนด้วย iterator ปกติ - แฮชของ ID คอมโพเนนต์ JPG ถูกส่งผ่าน
int_hashเพื่อนำไปใช้เลือกบัคเก็ต - เดิมทีมีเรื่องบังเอิญสองอย่างที่ลงตัวพร้อมกัน
- ผลของ
int_hashสำหรับค่า0,1,2มีความคงที่ - จำนวนบัคเก็ตของ
AK::HashTableพอดีกับการวางคอมโพเนนต์ให้อยู่ในลำดับที่ถูกต้อง
- ผลของ
- เพราะความบังเอิญนี้
JPGLoaderจึงอ่านสตรีม Huffman ให้แต่ละคอมโพเนนต์ตามลำดับที่ถูกต้อง และทำให้บั๊กถูกซ่อนไว้มาตั้งแต่แรก - เมื่อมีการเพิ่ม
malloc_good_size()แล้วจำนวนบัคเก็ตของHashTableเปลี่ยน ลำดับคอมโพเนนต์ก็เปลี่ยนตาม และภาพที่ได้จึงมีช่องสีแดงกับน้ำเงินสลับกัน
การแก้สุดท้ายด้วยการวนซ้ำแบบกำหนดลำดับแน่นอน
- หลังดีบักอยู่นานประมาณ 10 ชั่วโมง ก็ได้คอมมิตแก้ไขออกมา
- คอมมิตแก้ไขคือ
a10ad24c760bfe713f1493e49dff7da16d14bf39- ชื่อ:
LibGfx: Make JPGLoader iterate components deterministically - เวลาสร้าง: 31 พฤษภาคม 2021
- ชื่อ:
- แก่นของการแก้คือทำให้
JPGLoaderวนคอมโพเนนต์ในแบบ กำหนดลำดับได้แน่นอน - การสลับลำดับอาร์กิวเมนต์ของ
Colorอย่างเดียว แม้จะทำให้ภาพดูเหมือนถูกต้องในตอนนั้น แต่ถ้าในอนาคตมีการเปลี่ยนแปลงอื่นที่ทำให้ลำดับการวนเปลี่ยนอีก ก็อาจพังได้เหมือนเดิม - นี่เป็นตัวอย่างของปัญหาที่ดูเหมือนเป็นแค่ข้อผิดพลาดเล็กๆ ด้านการแสดงผล แต่จริงๆ แล้วเกิดจากการพึ่งพาลำดับการวนของคอนเทนเนอร์อย่างผิดวิธี ร่วมกับการเปลี่ยนแปลงขนาดการจัดสรรหน่วยความจำที่ไปเปิดโปงมัน
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
นี่คือหนึ่งในเหตุผลที่การใช้งาน hash table จำนวนมากใส่องค์ประกอบแบบสุ่มเข้าไปในอัลกอริทึม
เพราะลำดับขององค์ประกอบจะเปลี่ยนไปทุกครั้งที่รัน หากเผลอพึ่งพาลำดับโดยไม่ตั้งใจ ปัญหาก็จะโผล่มาให้เห็นอย่างรวดเร็ว
ถ้าอัลกอริทึมแฮชคงที่ ก็สามารถสร้างคีย์ที่ไปรวมอยู่ในบักเก็ตเดียวกันเพื่อใช้โจมตีแบบ denial-of-service ได้ ซึ่งวิธีนี้ยังช่วยป้องกันปัญหาด้านความปลอดภัยแบบนั้นได้ค่อนข้างดีด้วย
ผมชอบแบบนี้มากกว่า เพราะไม่ต้องตัดสินใจทุกครั้งว่าต้องใช้ map ที่เรียงลำดับแล้วหรือ map ที่ไม่เรียงลำดับ
เคยมีหลายครั้งที่คิดว่าใช้ map แบบไม่เรียงลำดับก็พอ แต่กลับผิดด้วยเหตุผลเล็ก ๆ น้อย ๆ
ไม่อย่างนั้นมันเป็นความคิดที่แย่มาก เพราะจะทำให้การดีบักปัญหาอื่นยากขึ้นมาก
ความสุ่มไม่ใช่เพื่อน แต่เป็นศัตรู
ประมาณ 20 ปีก่อน มีวิธีโจมตีเว็บเซิร์ฟเวอร์ Java โดยปรับแต่งพารามิเตอร์ URL ให้ทั้งหมดตกไปอยู่ในบักเก็ตเดียวกัน และกลายเป็นการโจมตี denial-of-service ครั้งใหญ่
ถ้าจำไม่ผิด เว็บเซิร์ฟเวอร์ PHP ก็เจอปัญหาความปลอดภัยแบบเดียวกันเป๊ะ
แก้ด้วยการใส่ seed ให้ hash table และแน่นอนว่า seed นั้นนักพัฒนาควบคุมได้ เพราะความสุ่มไม่ใช่เพื่อน แต่เป็นศัตรู
เรื่องนี้ดูเหมือนเป็นกรณีที่ถ้าดีบักเพิ่มอีกหน่อย แทนที่จะทำ bisect แบบ binary search ไปเรื่อย ๆ ก็น่าจะประหยัดเวลาได้
สุดท้ายก็ยังต้องใส่ log ที่พิมพ์ลำดับของคอมโพเนนต์อยู่ดี
การดีบักก็ดี แต่ ข้อความ commit ก็ยอดเยี่ยมเช่นกัน
สรุปสาเหตุและการแก้ไขไว้ได้ดีภายในไม่กี่ย่อหน้า
ถ้ารอนานพอ C++ ก็จะมีฟีเจอร์ที่เทียบเท่ากับ
malloc_good_sizeเข้ามาhttps://github.com/cplusplus/papers/issues/18
ชื่อเรื่องต้องมี [2021]
นี่ไม่ใช่ความผิดของ Gunnar ปัญหาอยู่ที่คนที่เอา ข้อมูลที่มีลำดับ ไปเก็บไว้ในไฟล์แฮช
ตลอดหลายสิบปีที่ทำงานนี้มา ผมเจอสถานการณ์ที่เมื่อ layout หน่วยความจำเปลี่ยนแล้วบั๊กที่ซ่อนอยู่ก็ถูกเผยออกมาหลายครั้ง
แต่ละครั้งต้องใช้เวลาดีบักตั้งแต่หลายชั่วโมงไปจนถึงหลายวัน
ถ้าการเขียนโปรแกรมไม่ยาก เราก็คงไม่จำเป็น แต่ก็ไม่รู้ว่าประโยคนี้จะยืนอยู่ได้นานแค่ไหนในยุคของ large language models
Gunnar ปรับปรุงบางอย่างให้ดีขึ้น และในกระบวนการนั้นก็แค่ทำให้ปัญหาของโค้ดเก่าที่พังอยู่แล้วโผล่ออกมา
แต่รางวัลตอบแทนของความพยายามนั้นกลับเป็นคำพูดอย่าง “Gunnar, I like you, but please don't make me go through this again. :^)”
เท่าที่รู้ SerenityOS มีทรัพยากรสำหรับทดสอบ หรือมีคนที่ช่วยกันเรื่องพีซีอยู่
การบอกว่าสร้าง SerenityOS ตั้งแต่ต้น 4–5 ครั้งบนแล็ปท็อป Sandy Bridge Mobile ปี 2011 ก็คล้ายกับการพยายาม พัฒนา Windows Vista บนคอมพิวเตอร์ที่ออกมาในช่วงระหว่าง Windows 3.1 กับ Windows 95
หลังปี 2011 CPU ไม่ได้เปลี่ยนไปมากขนาดนั้นในเชิงสัมพัทธ์ แต่ช่วงระหว่าง Windows 3.1 ถึง Vista นั้น x64 เริ่มแพร่หลาย และ CPU หลายคอร์ ก็กลายเป็นเรื่องปกติ
Vista เปิดตัวทั่วโลกต้นปี 2007 ดังนั้น CPU ที่มีอายุ 13 ปี ณ เวลาที่เปิดตัว ก็คือรุ่นปี 1994 ซึ่งเป็นช่วงประมาณหนึ่งปีหลังจาก Pentium รุ่นแรกออกมา
ตอนนั้นยังมีคนจำนวนมากใช้ 486 DX2-66 ที่ไว้ใจได้อยู่
น่าประทับใจทีเดียวที่ CPU อายุ 13 ปียังใช้ทำงานกับโปรเจกต์สมัยใหม่ในปัจจุบันได้ ตอนนั้นคงพูดแบบเดียวกันได้ยาก
หวังว่า CPU ที่ออกวันนี้จะยังใช้งานได้อย่างน่าพอใจไปจนหลังปี 2037
Visual Studio ก็รันได้ดี Photoshop ก็มีแค่เครื่องมือ AI ในระบบที่หน่วงนิดหน่อย
น่าจะเปิดแท็บ Chrome ไว้ราว 200 แท็บ พร้อมกับ Slack, WhatsApp และเบราว์เซอร์สำหรับทดสอบอีก 3 ตัว
CapCut ถ้าตัดต่อ 4K ก็อยากให้เร็วขึ้นอีกหน่อย แต่โปรเจกต์ 2K ที่ซับซ้อนก็ยังรับไหวสบาย
มีแค่โปรเจกต์ After Effects ที่ซับซ้อนเท่านั้นที่เริ่มชนขีดจำกัด มันไม่ค่อยชอบงานนั้น
คงต้องอัปเกรดแล้ว แต่สำหรับระบบที่แทบจะเก็บมาจากถังขยะ ถือว่าใช้ได้ดีทีเดียว
เห็น “Alien Lenna” แล้วรู้สึก เดจาวู ที่แท้ก็เป็นบทความที่เคยอ่านและเคยคอมเมนต์ไว้ก่อนหน้านี้จริง ๆ
https://news.ycombinator.com/item?id=27374942 (2021)