1 điểm bởi GN⁺ 3 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Binary x86_64-unknown-linux-musl của Ripgrep 15.2.0 thỉnh thoảng thoát với SIGSEGV khi tìm kiếm cây tệp quy mô lớn với mức đồng thời cao
  • Sự cố xảy ra bên trong calloc do opendir gọi, và điểm kiểm tra tính toàn vẹn metadata heap của musl mallocng xuất hiện ở đầu stack trace
  • Môi trường tái hiện là một cây gồm khoảng 20GiB·1,8 triệu tệp, lặp lại việc tìm kiếm bằng rg một chuỗi không tồn tại
  • Trên hệ thống 24 nhân, nếu có đủ RAM để cây tìm kiếm nằm trong kernel block cache, vấn đề thường xảy ra trong khoảng 1 phút
  • Vấn đề được tái hiện độc lập không chỉ với rg đi kèm OpenAI Codex mà còn với binary giống hệt từng byte với bản phát hành chính thức, xác nhận đây là vấn đề không phụ thuộc vào Codex

Môi trường phát sinh

  • Phiên bản sử dụng là ripgrep 15.2.0 rev e89fff8, bao gồm tính năng +pcre2
    • SIMD khi biên dịch: +SSE2,-SSSE3,-AVX2
    • SIMD khi chạy: +SSE2,+SSSE3,+AVX2
    • Có thể sử dụng PCRE2 10.45 và JIT
  • Hệ điều hành là OpenSUSE Tumbleweed Linux x86_64
  • Binary rg trong gói OpenAI Codex được phát hiện ban đầu giống hệt từng byte với bản phát hành x86_64-unknown-linux-musl chính thức
  • Sự cố cũng được tái hiện với binary chính thức, độc lập với Codex; binary dùng cho phân tích được build kèm debug symbol bằng lệnh sau
    • CROSS_CONTAINER_ENGINE=podman CARGO_PROFILE_RELEASE_DEBUG=true ~/.cargo/bin/cross build --release --target x86_64-unknown-linux-musl

Quy trình tái hiện

  • generate_repro_tree.py tạo một cây tệp ngẫu nhiên mô phỏng thống kê của repository nơi vấn đề ban đầu xảy ra
    • Chương trình này được viết bằng LLM
    • Kết quả tạo ra có quy mô khoảng 20GiB, 1,8 triệu tệp
  • Từ root của cây đã tạo, lặp lại tìm kiếm một chuỗi tùy ý không tồn tại
    • while true; do rg tnoheueunotshisnthukoethnsueothnsiuothonesuioseuinth; done
  • Quan sát cho thấy cây tìm kiếm đủ lớn là điều kiện thiết yếu để tái hiện
  • Trên hệ thống 24 nhân, nếu có đủ RAM trống để toàn bộ cây nằm trong kernel block cache, thường sẽ crash sau khoảng 1 phút

Vị trí crash

  • Kết quả thực tế là SIGSEGV để lại core dump
  • Đỉnh stack trace là get_meta của musl mallocng, crash tại điểm kiểm tra tính toàn vẹn metadata heap
  • Luồng gọi là opendir gọi calloc, rồi tiếp tục tới cơ chế duyệt thư mục của thư viện chuẩn Rust và worker ignore::walk của ripgrep
    • get_meta__malloc_allzeropcallocopendir
    • std::fs::read_dirignore::walk::Work::read_dirignore::walk::Worker::run
  • Tài liệu phân tích có đính kèm core dumpbinary rg tương ứng

Hành vi kỳ vọng và trạng thái hiện tại

  • Hành vi kỳ vọng là chạy mà không có lỗi segmentation ngay cả khi tìm kiếm quy mô lớn với mức đồng thời cao
  • Nội dung được cung cấp không bao gồm kết luận nguyên nhân, bản sửa, kết quả rà soát hay tình trạng giải quyết cuối cùng

1 bình luận

 
Ý kiến trên Hacker News
  • Có một đoạn thú vị trong bản vá kernel: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...

    Tôi đã thấy một báo cáo lỗi thú vị từ ripgrep, cùng với một bản phân tích do AI tạo ra khá chăm chỉ nhưng cũng khá tệ
    Đây là nói đến https://github.com/dfoxfranke/ripgrep-3494-analysis, và tôi cũng nghĩ rằng nó dài một cách khó tin nếu bảo là do con người viết. Hơn nữa, có vẻ chuỗi thảo luận này vừa được đăng ngay hôm nay

    • Đọc rất khổ sở, và trong cả bài dài dòng đó tôi không thấy chỗ nào chỉ ra được đoạn mã hay vùng nhớ giống như người thật trên lore.kernel đã tìm ra. Muốn hỏi ai hiểu Claude hơn: có đoạn nào thực sự xác định được nguyên nhân gốc không?
    • Cho đến khoảng năm 2000, người dùng điện thoại di động ở nơi công cộng bị xem là kiểu người khoe khoang đáng ghét, và giờ AI cũng đang đi qua một giai đoạn bị ghét bỏ khó chịu tương tự
      Nếu là 2 năm trước thì kiểu phân tích này có thể được xem là một đóng góp hào phóng về thời gian cho cộng đồng, nhưng giờ thì biết nguồn gốc và chi phí token chỉ có 0,06 đô la nên tôi chẳng muốn đọc nữa. Việc nó báo trước điều sẽ xảy ra trong tương lai cũng góp phần vào cảm giác đó
      Vài năm nữa, việc con người tự đào sâu lỗi sẽ thành phương án cuối cùng, còn các tác nhân AI khác sẽ đọc báo cáo để xác minh bản sửa lỗi. Cũng như việc bỏ qua assembly do compiler sinh ra và người dùng điện thoại ở nơi công cộng, có lẽ chúng ta sẽ chuyển từ giai đoạn chế giễu sang giai đoạn thờ ơ
  • Tôi hiểu chuyện cứ dùng allocator mặc định của musl vì tiện, nhưng với một ứng dụng mà tốc độ chính là mục tiêu, không đổi sang allocator nhanh hơn thì khá lạ
    mallocng yếu khi có tranh chấp đa luồng. Một ứng dụng vốn thường bị nghẽn I/O mà khi build với musl lại xuất hiện nút thắt malloc chỉ với 8 luồng; đổi sang mimalloc thì hiệu năng tăng 20 lần, gần như trở lại cấu hình mặc định của glibc, dù vẫn chậm hơn glibc+mimalloc một chút
    Đúng là ở đây có một vấn đề thú vị thật, nhưng lẽ ra nó không nên lộ ra theo cách này ngay từ đầu

    • ripgrep thực ra có chỉ định jemalloc làm global allocator khi build cho musl 64-bit: https://github.com/BurntSushi/ripgrep/blob/435f59fc4b43af3ab...
    • Nhìn vào stack của lỗi segmentation fault thì việc cấp phát xảy ra trong opendir của musl libc. Cách Rust thay allocator không phải là đổi allocator cho toàn bộ tiến trình, mà là chỉ thay allocator mà mã Rust gọi tới
      Tuy vậy, nếu vẫn phải đi qua một allocator dùng khóa toàn cục thì có vẻ ripgrep nên tránh dùng opendir của libc
    • Đây là lỗi kernel. Tôi đồng ý rằng các allocator của libc thường tệ mà không có lý do gì, nhưng vấn đề này có vẻ cũng có thể xảy ra với mimalloc, glibc hay mã ứng dụng khác với xác suất tương tự
    • Đa số chương trình tái sử dụng các vùng cấp phát để tăng tốc. Nếu không cấp phát gì cả thì cũng chẳng cần allocator nhanh, mà bản thân công việc của ripgrep vốn không nhất thiết đòi hỏi cấp phát quá thường xuyên
    • Ngược lại, chính các tính năng hardening mà mallocng của musl cung cấp đã giúp phát hiện ra lỗi kernel này. Nếu không thì có khi nó đã âm thầm làm hỏng bộ nhớ suốt nhiều tháng mà chẳng ai để ý
  • Nếu bạn đang chạy ripgrep trên hệ thống tệp của một cụm HPC quy mô lớn, hãy dừng lại ngay và thiết kế lại luồng công việc. Kiểu tác vụ này tạo ra lượng lớn I/O nhỏ, mà đó chính là gót chân Achilles của hệ thống tệp cụm quy mô lớn
    Bạn đang đẩy công việc đáng ra phải xử lý ở tầng bộ nhớ băng thông cao của cụm sang tầng metadata của hệ thống tệp, và chỉ cần vài người dùng chạy cùng lúc cũng có thể làm tê liệt cả hệ thống tệp băng thông cao

    • Đây không phải cụm HPC, chỉ là btrfs trên máy trạm của tôi thôi
    • Tôi cũng từng tự hỏi liệu đây có phải nguyên nhân gốc tương tự đằng sau việc GitHub gần đây thiếu ổn định hơn không. Đột nhiên xuất hiện hàng tỷ thao tác với file nhỏ, bị khuếch đại bởi việc dùng AI, và object graph thì vốn dĩ phân mảnh nên cũng khó mà prefetch một trang rồi khiến các thao tác Git thông thường chỉ đụng tới các object trong đúng trang đó
      Nếu nội dung ở https://isolveproblems.substack.com/p/how-microsoft-vaporize... đúng dù chỉ một phần, thì chỉ cần một đường đi chưa được tối ưu trong lớp trừu tượng hệ thống tệp của Azure là mức sử dụng tăng vọt có thể biến thành bán kính ảnh hưởng sự cố khổng lồ
  • Có lẽ nên liên kết thẳng tới bản phân tích lỗi kernel: https://github.com/dfoxfranke/ripgrep-3494-analysis

    • Tôi đã định đọc nhưng bỏ cuộc ngay ở đoạn Headline đầu tiên
      Nó nối tiếp bằng những câu như: “giá trị mà luồng lưu vào một trang ẩn danh mới page fault biến mất khi cùng luồng đọc lại khoảng 10 lệnh sau đó, và backing của trang bị thay thế trong lúc hàm đang chạy”, “nếu đọc pagemap vào lúc fault thì backing là zero page của kernel”, “cơ chế này được localize vào tương tác giữa đường nhanh anonymous fault của khóa theo VMA và TLB shootdown của munmap đồng thời”
      Thật khó hiểu “backing” của một trang là gì, freshly-faulted hay “khoảng 10 lệnh sau” nghĩa là gì, hoặc cơ chế được “localize” như thế nào. Nó trông giống một đống từ ghép lại hơn là giải thích kỹ thuật
      Đây là ví dụ về tài liệu kỹ thuật tử tế: https://yifan.lu/2019/01/11/the-first-f00d-exploit/
    • Đoạn “overflow vẫn cứ là overflow, use-after-free vẫn cứ là use-after-free, và cuộc đua musl mask vẫn cứ là race” buồn cười như thơ máy tính
      Kết luận đại khái là có vấn đề ở tổ hợp Linux 7.0 và musl 1.2.5, nhưng việc tái hiện vẫn chỉ thành công chập chờn khi ép tải nặng trên cùng CPU Threadripper vật lý đó, nên vẫn chưa thể loại trừ lỗi phần cứng
    • Đọc báo cáo lỗi do AI tạo ra thật kinh khủng
    • Việc truy vết thì rất tốt nhưng phần giải thích lại vô nghĩa. Thêm flush TLB không thể là lỗi được; CPU có thể flush bất cứ lúc nào nó muốn. Lỗi thật sự có vẻ là đã tồn tại zero-page PTE vào lúc lẽ ra không được phép có
      Có vẻ đây là một race phức tạp do CPU bị migrate đúng lúc kỳ cục, hoặc là lỗi ở đường xóa page table khiến sai PTE bị lộ ra tạm thời. Tôi cũng không nghĩ PFN của zero page là 0
      Nếu đoán thì có thể trong quá trình tự xóa page table, CPU đã được phép đọc một bảng đã bị giải phóng và tái sử dụng thông qua các mục đã cache của cấu trúc phân trang cấp cao hơn. Tôi từng debug kiểu vấn đề này rồi, thực sự rất kinh khủng
    • Đây là kiểu phân tích rác thải LLM dài dòng điển hình. Có thể phân tích đó đúng, nhưng rất khó đọc kỹ, và nếu là con người phân tích thì chắc chỉ cần một phần năm độ dài đó
  • Tại sao lỗi này chỉ xảy ra với musl libc chứ không phải libc khác?

    • Chỉ là ngẫu nhiên thôi, và thậm chí còn chỉ xảy ra trên một máy duy nhất
    • Khả năng lớn là vì allocator của musl để lộ ngay cho ứng dụng một trang đơn vừa mới page fault. Các allocator khác thường preallocate nhiều trang cùng lúc nên cửa sổ thời gian của race condition hẹp hơn
  • Bình thường tôi sẽ nghi ngờ kích thước stack luồng của musl, nhưng không biết giờ đã xác nhận đây là lỗi kernel chưa?