Tools

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 bool không phải 0 hay 1.
  • Vi phạm điều kiện của intrinsic, như copy_nonoverlapping vớ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ốngCông cụLý do
Cấu trúc dữ liệu dùng con trỏ thô, unsafe thuần RustMiriPhá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àicargo-fuzzTìm đầu vào bất ngờ gây panic, tràn số, crash
Lớp FFI gọi thư viện Ccargo-fuzz + sanitizerMiri hỗ trợ FFI hạn chế; sanitizer nhìn được cả phía C
Code atomic, lock-free phức tạpMiri + loomREADME 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ó unsafe cầ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 test cho 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

  1. Miri: an undefined behavior detection tool for Rust — rust-lang (GitHub)
  2. Rust Fuzz Book: cargo-fuzz — rust-fuzz
  3. Rust Fuzz Book: Tutorial — rust-fuzz