1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ไบนารี 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 ได้
  • ระบบปฏิบัติการคือ 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 ใน ripgrep
    • get_meta__malloc_allzeropcallocopendir
    • std::fs::read_dirignore::walk::Work::read_dirignore::walk::Worker::run
  • มีไฟล์แนบสำหรับการวิเคราะห์คือ core dump และ ไบนารี rg ที่เกี่ยวข้อง

พฤติกรรมที่คาดหวังและสถานะปัจจุบัน

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

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

 
GN⁺ 3 시간 전
ความคิดเห็นบน Hacker News
  • มีประเด็นน่าสนใจในแพตช์เคอร์เนล: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...

    เห็นรายงานบั๊กที่น่าสนใจใน ripgrep และการวิเคราะห์ที่สร้างโดย AI ซึ่งตั้งใจดีแต่ค่อนข้างแย่มาก
    หมายถึง https://github.com/dfoxfranke/ripgrep-3494-analysis ซึ่งผมเองก็คิดว่า ยาวเกินกว่าจะเชื่อว่าเป็นคนเขียน แถมดูเหมือนเธรดนี้เพิ่งถูกโพสต์วันนี้เอง

    • อ่านแล้วทรมาน และในบทความยืดยาวนั้นก็ไม่เจอตรงไหนที่ชี้ไปยังโค้ดหรือบริเวณเดียวกับที่คนจริง ๆ ใน lore.kernel หาเจอเลย อยากถามคนที่เข้าใจ Claude ดีกว่าว่า มีส่วนไหนที่ระบุสาเหตุจริง ๆ ได้บ้างไหม?
    • จนถึงราวปี 2000 คนที่ใช้โทรศัพท์มือถือในที่สาธารณะมักถูกมองว่าเป็นพวกชอบอวดน่ารำคาญ และตอนนี้ AI ก็กำลังผ่าน ช่วงต่อต้านแบบไม่สบอารมณ์ คล้าย ๆ กัน
      ถ้าเป็นเมื่อ 2 ปีก่อน การวิเคราะห์แบบนี้คงถูกมองว่าเป็นผลจากการสละเวลาให้ชุมชนอย่างใจกว้าง แต่ตอนนี้พอรู้ว่าแหล่งที่มาและค่าโทเคนมีต้นทุนแค่ 0.06 ดอลลาร์ ก็เลยไม่อยากอ่าน อีกส่วนหนึ่งก็เพราะมันเป็นลางบอกสิ่งที่จะเกิดขึ้นต่อไป
      อีกไม่กี่ปี การที่คนลงมือไล่บั๊กเองจะกลายเป็นทางเลือกสุดท้าย และ AI agent ตัวอื่นจะอ่านรายงานเพื่อตรวจสอบแพตช์เอง เราน่าจะขยับจากช่วงล้อเลียนไปสู่ช่วงไม่สนใจ เหมือนที่เราเมิน assembly ที่คอมไพเลอร์สร้างขึ้นและคนใช้มือถือในที่สาธารณะ
  • เข้าใจได้ที่ใช้ allocator พื้นฐานของ musl ต่อไปเพราะสะดวก แต่ใน แอปพลิเคชันที่มีเป้าหมายคือความเร็วโดยตรง การไม่เปลี่ยนไปใช้ allocator ที่เร็วกว่าเป็นเรื่องแปลก
    mallocng อ่อนไหวต่อ contention ในหลายเธรด แอปพลิเคชันที่ปกติคอขวดอยู่ที่ I/O พอบิลด์ด้วย musl ก็เกิดคอขวดที่ malloc ด้วยเธรดแค่ 8 ตัว และเมื่อเปลี่ยนไปใช้ mimalloc ประสิทธิภาพก็ ดีขึ้น 20 เท่า จนใกล้เคียงกับค่าพื้นฐานของ glibc มาก และช้ากว่า glibc+mimalloc เพียงเล็กน้อย
    จริงอยู่ว่ามีปัญหาที่น่าสนใจอยู่ แต่ตั้งแต่แรกมันไม่ควรถูกเผยออกมาในลักษณะนี้

    • จริง ๆ แล้ว ripgrep กำหนดให้ jemalloc เป็น global allocator เมื่อตอนบิลด์ด้วย musl แบบ 64 บิต: https://github.com/BurntSushi/ripgrep/blob/435f59fc4b43af3ab...
    • ดูจากสแตกของ segmentation fault การจัดสรรเกิดขึ้นใน opendir ของ musl libc วิธีเปลี่ยน allocator ที่ Rust ใช้ไม่ได้เปลี่ยน allocator ของทั้งโปรเซส แต่ เปลี่ยนเฉพาะ allocator ที่โค้ด Rust เรียกใช้
      อย่างไรก็ตาม ถ้าต้องผ่าน allocator ที่ใช้ global lock ก็ดูเหมือนว่า ripgrep ควรหลีกเลี่ยงการใช้ opendir ของ libc
    • นี่คือ บั๊กของเคอร์เนล ผมเห็นด้วยว่า allocator ของ libc มักแย่อย่างไม่มีเหตุผลนัก แต่ปัญหานี้ดูเหมือนอาจเกิดขึ้นได้ด้วยความน่าจะเป็นใกล้เคียงกันในโค้ดแอปพลิเคชันอื่น ๆ รวมถึง mimalloc หรือ glibc
    • โปรแกรมส่วนใหญ่เพิ่มความเร็วด้วยการ นำ allocation กลับมาใช้ซ้ำ ถ้าไม่ allocate เลย ก็ไม่ต้องใช้ allocator ที่เร็ว และงานของ ripgrep เองก็ไม่ได้จำเป็นโดยธรรมชาติที่จะต้อง allocate บ่อย ๆ
    • ตรงกันข้าม ฟีเจอร์เสริมความแข็งแรง ที่ mallocng ของ musl มีต่างหากที่ช่วยให้ค้นพบบั๊กเคอร์เนลนี้ได้ ไม่อย่างนั้นมันอาจทำลายหน่วยความจำเงียบ ๆ อยู่หลายเดือนโดยไม่ถูกสังเกตเห็นก็ได้
  • ถ้าคุณรัน ripgrep กับระบบไฟล์คลัสเตอร์ขนาดใหญ่บนคลัสเตอร์ HPC ก็ควรหยุดทันทีและออกแบบ workflow ใหม่ งานแบบนี้สร้าง I/O ขนาดเล็กจำนวนมาก ซึ่งเป็น จุดอ่อนสำคัญของระบบไฟล์คลัสเตอร์ขนาดใหญ่
    มันเป็นการโยนงานที่ควรจัดการในลำดับชั้นหน่วยความจำแบนด์วิดท์สูงของคลัสเตอร์ไปให้ลำดับชั้น metadata ของระบบไฟล์ และถ้ามีผู้ใช้เพียงไม่กี่คนรันพร้อมกัน ก็อาจทำให้ระบบไฟล์แบนด์วิดท์สูงทั้งระบบเป็นอัมพาตได้

    • นี่ไม่ใช่คลัสเตอร์ HPC เป็นแค่ btrfs ที่ผมใช้บนเวิร์กสเตชันของตัวเอง
    • ผมสงสัยว่าต้นตอที่ช่วงหลัง GitHub ไม่เสถียรอาจคล้ายกันหรือเปล่า ตอนนี้มีงานกับไฟล์เล็ก ๆ นับหมื่นล้านที่ถูกขยายผลจากการใช้ AI เกิดขึ้นอย่างฉับพลัน และ object graph ก็แตกกระจายโดยธรรมชาติ จึงยากที่จะ prefetch หน้าเดียวแล้วทำให้การทำงาน Git ทั่วไปแตะเฉพาะ object ภายในหน้านั้น
      ถ้าเนื้อหาใน 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/
    • ประโยค “overflow ก็ยัง overflow, use-after-free ก็ยัง use-after-free, และ race ของ mask ใน musl ก็ยัง race” ฟังเหมือนบทกวีคอมพิวเตอร์ดี เลยขำ
      สรุปคร่าว ๆ ดูเหมือนว่าปัญหาอยู่ที่ การผสมกันของ Linux 7.0 กับ musl 1.2.5 แต่การ reproduce ยังคงสำเร็จแบบเป็นครั้งคราวเท่านั้น เมื่อโหลดหนักบน CPU Threadripper ตัวจริงตัวเดิม จึงยังตัดปัญหาฮาร์ดแวร์ออกไม่ได้
    • การอ่าน รายงานบั๊กที่สร้างโดย AI เป็นประสบการณ์ที่เลวร้ายมาก
    • การไล่ร่องรอยทำได้ยอดเยี่ยม แต่คำอธิบายไม่สมเหตุสมผล การ flush TLB เพิ่มเติมไม่อาจเป็นข้อผิดพลาดได้ เพราะ CPU สามารถ flush เมื่อไรก็ได้ตามต้องการ ข้อผิดพลาดจริงดูเหมือนอยู่ที่การมี zero-page PTE ในเวลาที่ไม่ควรมี
      ดูเหมือนเป็น race condition ซับซ้อนที่เกิดจากการ migrate CPU ในจังหวะแปลก ๆ หรือเป็นบั๊กใน path การลบ page table ที่เผย PTE ผิดตัวออกมาชั่วคราว ผมก็ไม่คิดว่า PFN ของ zero page คือ 0 ด้วย
      ถ้าให้เดา อาจเป็นไปได้ว่าระหว่างกระบวนการลบ page table โดยตรง CPU ได้รับอนุญาตให้อ่าน table ที่ถูก free และ reuse แล้วผ่าน entry ที่ถูก cache ไว้ของโครงสร้าง paging ชั้นบน ผมเคยดีบักปัญหาแบบนี้มาก่อน และมันเลวร้ายจริง ๆ
    • เป็น การวิเคราะห์กาก ๆ สไตล์ LLM ที่ยืดยาวตามแบบฉบับ มันอาจวิเคราะห์ถูกก็ได้ แต่ยากที่จะอ่านละเอียด และถ้าเป็นคนทำการวิเคราะห์เดียวกันนี้ คงลดความยาวลงเหลือหนึ่งในห้า
  • ทำไมบั๊กถึงเกิดเฉพาะกับ musl libc ไม่ใช่ libc ตัวอื่น?

    • เป็นแค่ความบังเอิญ และยังเกิดเฉพาะบน เครื่องเดียว ด้วย
    • น่าจะเป็นเพราะ allocator ของ musl เปิดเผย page เดี่ยวที่เพิ่งเกิด page fault ให้แอปพลิเคชันทันที allocator ตัวอื่นมัก pre-allocate หลาย page พร้อมกัน จึงทำให้ หน้าต่างเวลาของ race condition แคบลง
  • ถ้าเป็นปกติผมคงสงสัย ขนาด thread stack ของ musl แต่สงสัยเหมือนกันว่ายืนยันแล้วหรือยังว่าเป็นบั๊กของเคอร์เนล