Kiểm thử unsafe Rust: Miri và cargo-fuzz bắt lỗi trình biên dịch bỏ qua
Tóm tắt
Lỗ hổng Rust thật đều nằm trong khối unsafe, nơi trình biên dịch không kiểm tra. Miri bắt use-after-free, đọc dữ liệu chưa khởi tạo, data race khi chạy test; cargo-fuzz tìm đầu vào gây crash. Đưa cả hai vào CI.
Khi chuyển từ C/C++ sang Rust, phần khó nhất không nằm ở code an toàn mà ở các khối unsafe: lớp FFI gọi thư viện C cũ, cấu trúc dữ liệu tự quản lý bộ nhớ, đoạn tối ưu hiệu năng. Lỗ hổng bộ nhớ đầu tiên trong Rust của Android và CVE Rust đầu tiên của nhân Linux đều nằm trong unsafe. Trình biên dịch không kiểm tra được phần này, nên bạn phải tự kiểm tra. Hai công cụ chính là Miri (phát hiện undefined behavior khi chạy test) và cargo-fuzz (tự sinh đầu vào để tìm crash).
Miri: chạy test trong một trình thông dịch "khó tính"
Miri là trình thông dịch cho Rust, chạy chương trình từng bước và kiểm tra mọi thao tác bộ nhớ. Nó bắt được:
- Truy cập ngoài biên và use-after-free.
- Đọc dữ liệu chưa khởi tạo.
- Truy cập bộ nhớ hoặc tham chiếu không căn chỉnh (misaligned).
- Phá bất biến kiểu cơ bản, ví dụ một
boolkhông phải 0 hay 1. - Vi phạm điều kiện của intrinsic, như
copy_nonoverlappingvới hai vùng chồng nhau. - Data race và một số hiệu ứng bộ nhớ yếu.
- Rò rỉ bộ nhớ, và (thử nghiệm) vi phạm quy tắc aliasing Stacked Borrows/Tree Borrows.
rustup +nightly component add miri
cargo miri test
# thử nhiều seed để khám phá thứ tự luồng và địa chỉ cấp phát khác nhau
MIRIFLAGS="-Zmiri-many-seeds=0..16" cargo miri test
cargo-fuzz: để máy tự nghĩ ra đầu vào xấu
cargo-fuzz không phải là fuzzer mà là công cụ gọi fuzzer libFuzzer qua crate libfuzzer-sys. Bạn viết một "fuzz target" nhận mảng byte bất kỳ và đưa vào hàm cần kiểm tra:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = my_crate::parse(s);
}
});
cargo fuzz init # tạo thư mục fuzz/
cargo fuzz list # liệt kê các target
cargo fuzz run parse # chạy đến khi gặp panic hoặc crash
Khi gặp lỗi, đầu vào gây crash được lưu vào fuzz/artifacts/ để bạn chạy lại và viết test hồi quy. Tài liệu khuyên kết hợp với AddressSanitizer để có báo cáo crash chi tiết hơn.
Khi nào dùng công cụ nào?
| Tình huống | Công cụ | Lý do |
|---|---|---|
| Cấu trúc dữ liệu dùng con trỏ thô, unsafe thuần Rust | Miri | Phát hiện UB chính xác ngay cả khi chương trình "chạy đúng" |
| Parser, decoder, xử lý dữ liệu từ bên ngoài | cargo-fuzz | Tìm đầu vào bất ngờ gây panic, tràn số, crash |
| Lớp FFI gọi thư viện C | cargo-fuzz + sanitizer | Miri hỗ trợ FFI hạn chế; sanitizer nhìn được cả phía C |
| Code atomic, lock-free phức tạp | Miri + loom | README Miri tự khuyên dùng loom vì mô phỏng bộ nhớ yếu của Miri chưa đầy đủ |
Giới hạn cần biết
- Miri chỉ thử một đường chạy mỗi lần: lỗi chỉ xuất hiện ở thứ tự luồng khác có thể bị bỏ sót; dùng nhiều seed để tăng độ phủ.
- Miri chậm và hạn chế: là trình thông dịch nên chậm hơn nhiều lần; hỗ trợ API hệ điều hành và FFI có hạn, không hỗ trợ mạng.
- Không có UB không có nghĩa là đúng: README Miri nói rõ việc Miri không thấy lỗi không chứng minh code là sound.
- Fuzzing cần thời gian chạy: vài phút chạy thử chỉ là khởi đầu; giá trị đến từ chạy liên tục và giữ lại corpus.
Áp dụng thế nào?
- Dev: Mỗi module có
unsafecần có test riêng chạy được dưới Miri. Mỗi hàm nhận dữ liệu từ bên ngoài (file, mạng, FFI) cần một fuzz target. - Tech Lead: Thêm job CI
cargo miri testcho các crate cóunsafe(dùng nightly riêng cho job này), và chạy fuzzing định kỳ, lưu crash thành test hồi quy. Đây là cách biến "review kỹ unsafe" thành quy trình kiểm chứng được. - PM: Khi nghe "đã viết lại bằng Rust nên an toàn", hãy hỏi thêm: bao nhiêu phần trăm là
unsafe, và phần đó có được chạy Miri và fuzz không?
Nguồn & tham khảo
- Miri: an undefined behavior detection tool for Rust — rust-lang (GitHub)
- Rust Fuzz Book: cargo-fuzz — rust-fuzz
- Rust Fuzz Book: Tutorial — rust-fuzz