- Binary x86_64-unknown-linux-musl của Ripgrep 15.2.0 thỉnh thoảng thoát với
SIGSEGVkhi 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
callocdoopendirgọ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
rgmộ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
- SIMD khi biên dịch:
- Hệ điều hành là OpenSUSE Tumbleweed Linux x86_64
- Binary
rgtrong 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_metacủa musl mallocng, crash tại điểm kiểm tra tính toàn vẹn metadata heap - Luồng gọi là
opendirgọicalloc, rồi tiếp tục tới cơ chế duyệt thư mục của thư viện chuẩn Rust và workerignore::walkcủa ripgrepget_meta→__malloc_allzerop→calloc→opendirstd::fs::read_dir→ignore::walk::Work::read_dir→ignore::walk::Worker::run
- Tài liệu phân tích có đính kèm core dump và binary 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-...
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
mallocchỉ 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
opendircủ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ớiTuy 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
opendircủa libcNế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
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
Headlineđầu tiênNó 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-faultedhay “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/
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ó 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
Tại sao lỗi này chỉ xảy ra với musl libc chứ không phải libc khác?
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?