System Design

Gia cố C++ khi chưa thể bỏ: libc++ hardening giảm 30% segfault ở Google

Tóm tắt

Google bật libc++ hardening cho toàn bộ server production: tìm hơn 1.000 bug, giảm 30% segfault, chi phí khoảng 0,3%. Một cờ build biến lỗi tràn biên thành crash an toàn cho phần C++ chưa thể viết lại.

Chuyển sang Rust là hướng dài hạn, nhưng thực tế mỗi công ty vẫn còn hàng triệu, có khi hàng trăm triệu dòng C++ sẽ sống thêm nhiều năm. Câu hỏi thực dụng là: trong lúc chờ, làm sao giảm lỗ hổng của chính đống code đó? Google đã trả lời bằng một thay đổi nghe rất nhỏ: bật kiểm tra biên trong thư viện chuẩn C++ (libc++ hardening) cho toàn bộ hệ thống server. Kết quả: hơn 1.000 bug được phát hiện, tỷ lệ segfault trong production giảm 30%, với chi phí hiệu năng trung bình chỉ 0,30%.

Vấn đề: C++ chuẩn không kiểm tra gì cả

Theo mặc định, v[i] trên std::vector không kiểm tra i có nằm trong mảng không; *opt trên std::optional rỗng đọc vùng nhớ chưa khởi tạo. Đó là undefined behavior, và là cửa cho lỗ hổng an toàn không gian (spatial safety): đọc/ghi ra ngoài vùng nhớ được cấp. Theo theo dõi của Project Zero mà Google dẫn lại, loại lỗi này chiếm khoảng 40% số khai thác lỗi bộ nhớ thực tế trong thập kỷ qua.

Các chế độ hardening của libc++

Chế độKiểm tra gìDùng khi nào
noneKhông kiểm tra (mặc định nếu nhà phân phối không đổi)Không nên cho code mới
fastNhóm kiểm tra quan trọng về bảo mật, chi phí thấp: truy cập phần tử hợp lệ, khoảng đầu vào hợp lệProduction (LLVM khuyến nghị)
extensivefast cộng thêm: con trỏ khác null, khoảng không chồng lấn, giải phóng hợp lệ, allocator tương thích…Khi đã đo benchmark và chấp nhận chi phí
debugMọi kiểm tra, kể cả heuristic tốn kém và assertion nội bộChỉ môi trường test

Bật bằng một macro, đặt trước mọi #include (thường qua cờ build):

clang++ -stdlib=libc++ \
  -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST \
  main.cpp

Khi kiểm tra thất bại ở chế độ fast/extensive, mặc định chương trình dừng ngay (trap). Một lỗi tràn biên vốn có thể bị khai thác âm thầm biến thành một crash rõ ràng, có thể gỡ lỗi.

Google triển khai thế nào và được gì?

  • Lộ trình: chạy trong môi trường test hơn một năm, giai đoạn "bake" ở test và pre-production, rồi triển khai dần ra production trong vài tháng. Chrome đã bật từ 2022.
  • Phạm vi: giờ là mặc định cho hệ thống server production, gồm Search, Gmail, Drive, YouTube, Maps; chỉ rất ít thành phần xin miễn.
  • Chi phí: trung bình 0,30%. Google còn tìm và gỡ một kiểm tra thừa (giảm từ 0,35% xuống 0,30%) và đóng góp ngược cho LLVM.
  • Lợi ích: hơn 1.000 bug lộ ra, có bug tồn tại hơn mười năm; dự kiến chặn 1.000–2.000 bug mới mỗi năm; segfault production giảm 30%; làm hỏng một bài tập red team nội bộ.

Rủi ro và lưu ý

  • Crash nhiều hơn lúc đầu: bug cũ đang "chạy tạm được" sẽ lộ ra thành crash. Vì vậy cần giai đoạn test dài trước production.
  • Chỉ bảo vệ code dùng thư viện chuẩn: mảng C thô, con trỏ số học tự viết không được kiểm tra. Cần thêm công cụ như cảnh báo -Wunsafe-buffer-usage của Clang để tìm chúng.
  • Không chặn use-after-free: hardening giải quyết an toàn không gian; an toàn thời gian (UAF) cần cơ chế khác như MiraclePtr hay chuyển sang Rust.
  • Chế độ observe: có chế độ chỉ ghi log rồi chạy tiếp, nhưng chạy tiếp sau lỗi vẫn là undefined behavior; chỉ dùng tạm khi đang chuyển đổi.
  • ABI: các chế độ hardening không đổi ABI, nhưng một số kiểm tra (như iterator có biên) phụ thuộc tuỳ chọn ABI của thư viện lúc build.

Áp dụng thế nào?

  • Dev: Bật chế độ debug trong CI và chế độ fast trong build release. Chạy bộ test hiện có: mỗi trap là một bug thật đang chờ được sửa.
  • Tech Lead: Làm theo lộ trình của Google: test, bake, rồi bật dần theo dịch vụ, có cơ chế miễn trừ có ghi lý do. Đo hiệu năng trên workload thật trước khi kết luận "quá chậm".
  • PM: Đây là khoản đầu tư rẻ nhất trong chiến lược an toàn bộ nhớ: không viết lại code, chi phí dưới 1%, lợi ích đo được ngay. Đưa nó vào lộ trình trước khi bàn chuyện viết lại bằng Rust.

Nguồn & tham khảo

  1. Retrofitting spatial safety to hundreds of millions of lines of C++ — Google Security Blog
  2. Hardening Modes — LLVM libc++ documentation