- ไบนารี x86_64-unknown-linux-musl ของ Ripgrep 15.2.0 จะจบการทำงานพร้อม
SIGSEGVเป็นครั้งคราวเมื่อค้นหาในโครงสร้างไฟล์ขนาดใหญ่ด้วยระดับความขนานสูง - การแครชเกิดขึ้นภายใน
callocที่ถูกเรียกโดยopendirและจุดตรวจสอบความถูกต้องของเมทาดาทาฮีปของ musl mallocng ปรากฏอยู่บนสุดของสแตกเทรซ - สภาพแวดล้อมที่ใช้ทดสอบการทำซ้ำคือโครงสร้างไฟล์ขนาดประมาณ 20GiB·1.8 ล้านไฟล์ และทำการค้นหาสตริงที่ไม่มีอยู่ด้วย
rgซ้ำ ๆ - บนระบบ 24 คอร์ หากมี RAM เพียงพอให้โครงสร้างสำหรับค้นหาอยู่ใน kernel block cache ได้ทั้งหมด ปัญหามักเกิดขึ้นภายใน ประมาณ 1 นาที
- พบว่าสามารถทำซ้ำได้อย่างอิสระไม่เพียงกับ
rgที่รวมอยู่ใน OpenAI Codex แต่รวมถึงไบนารีที่เหมือนกับรุ่นทางการทุกไบต์ด้วย จึงยืนยันได้ว่าเป็น ปัญหาที่ไม่เกี่ยวกับการพึ่งพา Codex
สภาพแวดล้อมที่เกิดปัญหา
- เวอร์ชันที่ใช้คือ ripgrep 15.2.0 rev
e89fff8และมีฟีเจอร์+pcre2- SIMD ตอนคอมไพล์:
+SSE2,-SSSE3,-AVX2 - SIMD ตอนรัน:
+SSE2,+SSSE3,+AVX2 - ใช้งาน PCRE2 10.45 และ JIT ได้
- SIMD ตอนคอมไพล์:
- ระบบปฏิบัติการคือ OpenSUSE Tumbleweed Linux x86_64
rgที่มากับ OpenAI Codex ซึ่งพบปัญหาเป็นครั้งแรก เหมือนกับ รีลีสทางการ x86_64-unknown-linux-musl ทุกไบต์- นอกจาก Codex แล้ว ยังทำซ้ำได้กับไบนารีทางการด้วย และไบนารีสำหรับวิเคราะห์ถูกบิวด์พร้อม debug symbols ด้วยคำสั่งต่อไปนี้
CROSS_CONTAINER_ENGINE=podman CARGO_PROFILE_RELEASE_DEBUG=true ~/.cargo/bin/cross build --release --target x86_64-unknown-linux-musl
ขั้นตอนการทำซ้ำ
- generate_repro_tree.py จะสร้างโครงสร้างไฟล์แบบสุ่มที่เลียนแบบสถิติของรีโพซิทอรีที่เดิมพบปัญหา
- โปรแกรมนี้เขียนโดย LLM
- ผลลัพธ์ที่สร้างมีขนาดประมาณ 20GiB และราว 1.8 ล้านไฟล์
- ที่รากของโครงสร้างที่สร้างขึ้น ให้ค้นหาสตริงสุ่มที่ไม่มีอยู่ซ้ำ ๆ
while true; do rg tnoheueunotshisnthukoethnsueothnsiuothonesuioseuinth; done
- มีข้อสังเกตว่าจำเป็นต้องใช้โครงสร้างสำหรับค้นหาที่มีขนาดใหญ่พอเพื่อให้ทำซ้ำได้
- บนระบบ 24 คอร์ หากมี RAM เหลือพอให้ทั้งโครงสร้างอยู่ใน kernel block cache ได้ทั้งหมด มักจะแครชหลังผ่านไปประมาณ 1 นาที
จุดที่เกิดการแครช
- ผลลัพธ์จริงคือ
SIGSEGVที่ทิ้ง core dump ไว้ - ด้านบนสุดของสแตกเทรซคือ
get_metaของ musl mallocng และแครชที่จุดตรวจสอบความถูกต้องของเมทาดาทาฮีป - ลำดับการเรียกคือ
opendirเรียกcallocแล้วต่อไปยังการเดินไดเรกทอรีของ Rust standard library และ worker ของignore::walkใน ripgrepget_meta→__malloc_allzerop→calloc→opendirstd::fs::read_dir→ignore::walk::Work::read_dir→ignore::walk::Worker::run
- มีไฟล์แนบสำหรับการวิเคราะห์คือ core dump และ ไบนารี rg ที่เกี่ยวข้อง
พฤติกรรมที่คาดหวังและสถานะปัจจุบัน
- พฤติกรรมที่คาดหวังคือสามารถ ทำงานได้โดยไม่เกิดข้อผิดพลาดเซกเมนเทชัน แม้เป็นการค้นหาขนาดใหญ่และมีความขนานสูง
- เนื้อหาที่ให้มายังไม่มีการยืนยันสาเหตุ ข้อเสนอการแก้ไข ผลการตรวจสอบ หรือสถานะการแก้ปัญหาขั้นสุดท้าย
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
มีประเด็นน่าสนใจในแพตช์เคอร์เนล: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...
ถ้าเป็นเมื่อ 2 ปีก่อน การวิเคราะห์แบบนี้คงถูกมองว่าเป็นผลจากการสละเวลาให้ชุมชนอย่างใจกว้าง แต่ตอนนี้พอรู้ว่าแหล่งที่มาและค่าโทเคนมีต้นทุนแค่ 0.06 ดอลลาร์ ก็เลยไม่อยากอ่าน อีกส่วนหนึ่งก็เพราะมันเป็นลางบอกสิ่งที่จะเกิดขึ้นต่อไป
อีกไม่กี่ปี การที่คนลงมือไล่บั๊กเองจะกลายเป็นทางเลือกสุดท้าย และ AI agent ตัวอื่นจะอ่านรายงานเพื่อตรวจสอบแพตช์เอง เราน่าจะขยับจากช่วงล้อเลียนไปสู่ช่วงไม่สนใจ เหมือนที่เราเมิน assembly ที่คอมไพเลอร์สร้างขึ้นและคนใช้มือถือในที่สาธารณะ
เข้าใจได้ที่ใช้ allocator พื้นฐานของ musl ต่อไปเพราะสะดวก แต่ใน แอปพลิเคชันที่มีเป้าหมายคือความเร็วโดยตรง การไม่เปลี่ยนไปใช้ allocator ที่เร็วกว่าเป็นเรื่องแปลก
mallocng อ่อนไหวต่อ contention ในหลายเธรด แอปพลิเคชันที่ปกติคอขวดอยู่ที่ I/O พอบิลด์ด้วย musl ก็เกิดคอขวดที่
mallocด้วยเธรดแค่ 8 ตัว และเมื่อเปลี่ยนไปใช้ mimalloc ประสิทธิภาพก็ ดีขึ้น 20 เท่า จนใกล้เคียงกับค่าพื้นฐานของ glibc มาก และช้ากว่า glibc+mimalloc เพียงเล็กน้อยจริงอยู่ว่ามีปัญหาที่น่าสนใจอยู่ แต่ตั้งแต่แรกมันไม่ควรถูกเผยออกมาในลักษณะนี้
opendirของ musl libc วิธีเปลี่ยน allocator ที่ Rust ใช้ไม่ได้เปลี่ยน allocator ของทั้งโปรเซส แต่ เปลี่ยนเฉพาะ allocator ที่โค้ด Rust เรียกใช้อย่างไรก็ตาม ถ้าต้องผ่าน allocator ที่ใช้ global lock ก็ดูเหมือนว่า ripgrep ควรหลีกเลี่ยงการใช้
opendirของ libcถ้าคุณรัน ripgrep กับระบบไฟล์คลัสเตอร์ขนาดใหญ่บนคลัสเตอร์ HPC ก็ควรหยุดทันทีและออกแบบ workflow ใหม่ งานแบบนี้สร้าง I/O ขนาดเล็กจำนวนมาก ซึ่งเป็น จุดอ่อนสำคัญของระบบไฟล์คลัสเตอร์ขนาดใหญ่
มันเป็นการโยนงานที่ควรจัดการในลำดับชั้นหน่วยความจำแบนด์วิดท์สูงของคลัสเตอร์ไปให้ลำดับชั้น metadata ของระบบไฟล์ และถ้ามีผู้ใช้เพียงไม่กี่คนรันพร้อมกัน ก็อาจทำให้ระบบไฟล์แบนด์วิดท์สูงทั้งระบบเป็นอัมพาตได้
ถ้าเนื้อหาใน https://isolveproblems.substack.com/p/how-microsoft-vaporize... ถูกต้องแม้เพียงบางส่วน แค่ path ที่ไม่ได้ optimize สักเส้นเดียวใน abstraction ของระบบไฟล์บน Azure ก็อาจทำให้การใช้งานที่พุ่งขึ้นกลายเป็น blast radius ขนาดมหาศาล ได้
อาจดีกว่าถ้าลิงก์ไปยังการวิเคราะห์บั๊กเคอร์เนลโดยตรง: https://github.com/dfoxfranke/ripgrep-3494-analysis
Headlineแรกมีประโยคต่อ ๆ กันอย่าง “ค่าที่เธรดเก็บไว้ใน anonymous page ที่เพิ่งเกิด page fault หายไปเมื่อเธรดเดิมอ่านซ้ำหลังจากนั้นราว 10 คำสั่ง และ backing ของ page ถูกเปลี่ยนระหว่างการทำงานของฟังก์ชัน”, “ถ้าอ่าน pagemap ณ ตอน fault จะเห็นว่า backing เป็น zero page ของเคอร์เนล”, “กลไกถูก localize ไปที่ปฏิสัมพันธ์ระหว่าง fast path ของ anonymous fault ใน per-VMA lock กับ TLB shootdown ของ
munmapพร้อมกัน”ยากที่จะเข้าใจว่า
backingของ page คืออะไร,freshly-faultedหรือ “หลังจากนั้นราว 10 คำสั่ง” หมายถึงอะไร หรือกลไกถูก “localize” ได้อย่างไร ดูเหมือน เอาคำมาเรียงต่อกัน มากกว่าจะเป็นคำอธิบายทางเทคนิคตัวอย่างเอกสารเทคนิคที่ดีคืออันนี้: https://yifan.lu/2019/01/11/the-first-f00d-exploit/
สรุปคร่าว ๆ ดูเหมือนว่าปัญหาอยู่ที่ การผสมกันของ Linux 7.0 กับ musl 1.2.5 แต่การ reproduce ยังคงสำเร็จแบบเป็นครั้งคราวเท่านั้น เมื่อโหลดหนักบน CPU Threadripper ตัวจริงตัวเดิม จึงยังตัดปัญหาฮาร์ดแวร์ออกไม่ได้
ดูเหมือนเป็น race condition ซับซ้อนที่เกิดจากการ migrate CPU ในจังหวะแปลก ๆ หรือเป็นบั๊กใน path การลบ page table ที่เผย PTE ผิดตัวออกมาชั่วคราว ผมก็ไม่คิดว่า PFN ของ zero page คือ 0 ด้วย
ถ้าให้เดา อาจเป็นไปได้ว่าระหว่างกระบวนการลบ page table โดยตรง CPU ได้รับอนุญาตให้อ่าน table ที่ถูก free และ reuse แล้วผ่าน entry ที่ถูก cache ไว้ของโครงสร้าง paging ชั้นบน ผมเคยดีบักปัญหาแบบนี้มาก่อน และมันเลวร้ายจริง ๆ
ทำไมบั๊กถึงเกิดเฉพาะกับ musl libc ไม่ใช่ libc ตัวอื่น?
ถ้าเป็นปกติผมคงสงสัย ขนาด thread stack ของ musl แต่สงสัยเหมือนกันว่ายืนยันแล้วหรือยังว่าเป็นบั๊กของเคอร์เนล