Fil-C: Mô hình đơn giản hóa giúp C/C++ trở nên an toàn về bộ nhớ

17 tháng 4, 2026·5 phút đọc

Bài viết khám phá Fil-C, một mô hình nhằm biến C/C++ trở nên an toàn về bộ nhớ thông qua việc tự động viết lại mã nguồn và thêm cơ chế kiểm tra giới hạn. Bằng cách sử dụng cấu trúc AllocationRecord và tích hợp Garbage Collector, Fil-C giúp ngăn chặn các lỗi bộ nhớ phổ biến như buffer overflow hay memory leak, dù phải đánh đổi một phần hiệu năng.

Gần đây, cộng đồng lập trình viên đang bàn luận sôi nổi về Fil-C, một dự án tự định vị là bản triển khai C/C++ an toàn về bộ nhớ (memory-safe). Mặc dù phiên bản thực tế của Fil-C hoạt động phức tạp thông qua việc viết lại LLVM IR, nhưng việc tìm hiểu một mô hình đơn giản hóa sẽ giúp chúng ta nắm bắt dễ dàng hơn về bản chất cơ chế hoạt động của nó.

Mô hình đơn giản hóa này hoạt động bằng cách tự động chuyển đổi mã nguồn C/C++ không an toàn thành mã an toàn. Thay vì can thiệp sâu vào tầng IR, chúng ta có thể hình dung nó như một bộ viết lại mã nguồn thông minh.

Cơ chế AllocationRecord

Trong Fil-C, mọi biến cục bộ kiểu con trỏ đều được "đôi" với một biến cục bộ khác có kiểu AllocationRecord*. Cấu trúc này được định nghĩa大致 như sau:

struct AllocationRecord {
    char* visible_bytes;
    char* invisible_bytes;
    size_t length;
};

Khi bạn khai báo một con trỏ, bộ chuyển đổi sẽ tự động thêm biến record đi kèm:

// Mã gốc
void f() {
    T1* p1;
    ...
}

// Sau khi chuyển đổi bởi Fil-C
void f() {
    T1* p1; AllocationRecord* p1ar = NULL;
    ...
}

Chuyển đổi các thao tác con trỏ

Các thao tác gán con trỏ đơn giản cũng được mở rộng để bao gồm cả việc di chuyển AllocationRecord*:

  • p1 = p2; trở thành p1 = p2, p1ar = p2ar;
  • p1 = (T1*)x; trở thành p1 = (T1*)x, p1ar = NULL;

Đặc biệt, các hàm thư viện chuẩn như mallocfree cũng được thay thế bằng phiên bản Fil-C:

// Mã gốc
p1 = malloc(x);
free(p1);

// Mã Fil-C
{p1, p1ar} = filc_malloc(x);
filc_free(p1, p1ar);

Cấp phát và kiểm tra giới hạn

Hàm filc_malloc thực sự thực hiện ba lần cấp phát: một cho AllocationRecord, một cho vùng nhớ dữ liệu (visible_bytes) và một cho vùng nhớ siêu dữ liệu (invisible_bytes).

Khi một con trỏ được giải tham chiếu (dereferenced), AllocationRecord* đi kèm sẽ được sử dụng để kiểm tra giới hạn (bounds checking). Điều này đảm bảo rằng chương trình không truy cập vào vùng nhớ ngoài phạm vi cho phép, ngăn chặn lỗi buffer overflow.

Xử lý con trỏ trong Heap và vai trò của Invisible Bytes

Vấn đề trở nên thú vị hơn khi giá trị được lưu trữ chính là một con trỏ khác. Nếu con trỏ nằm trong heap (không phải biến cục bộ), compiler không thể tự động thêm biến record đi kèm. Đây là lúc invisible_bytes phát huy tác dụng.

Nếu có một con trỏ tại vị trí visible_bytes + i, thì siêu dữ liệu đi kèm của nó sẽ nằm tại invisible_bytes + i. Cơ chế này cho phép Fil-C theo dõi nguồn gốc của các con trỏ lồng nhau, đảm bảo tính toàn vẹn của bộ nhớ ngay cả khi con trỏ được truyền qua lại giữa các hàm.

Sự xuất hiện của Garbage Collector (GC)

Một điểm đáng ngạc nhiên là Fil-C tích hợp Garbage Collector (Bộ thu gom rác) vào C/C++. Hàm filc_free không giải phóng ngay lập tức đối tượng AllocationRecord. Thay vào đó, GC sẽ quét các đối tượng này và giải phóng những cái không thể tiếp cận được (unreachable).

Điều này có nghĩa là:

  1. Quên gọi free không còn gây rò rỉ bộ nhớ (memory leak) nghiêm trọng nữa vì GC sẽ dọn dẹp.
  2. Việc gọi free vẫn hữu ích để giải phóng bộ nhớ sớm hơn, giúp GC giảm tải.

Ngoài ra, nếu địa chỉ của một biến cục bộ bị lấy và sử dụng sau khi biến đó hết phạm vi (scope), Fil-C sẽ tự động "nâng cấp" biến đó lên heap để đảm bảo an toàn.

Các vấn đề phức tạp hơn trong phiên bản sản xuất

Mô hình đơn giản hóa này bỏ qua một số thách thức lớn mà phiên bản thực tế của Fil-C phải giải quyết:

  • Luồng (Threads): Tính đồng thời làm cho GC trở nên phức tạp hơn. Các thao tác nguyên tử (atomic operations) trên con trỏ cũng cần xử lý đặc biệt vì việc ghi hai lần (con trỏ và record) sẽ phá vỡ tính nguyên tử.
  • Con trỏ hàm: Cần thêm siêu dữ liệu để phân biệt con trỏ hàm và con trỏ dữ liệu, ngăn chặn các cuộc tấn công nhầm lẫn kiểu dữ liệu.
  • Tối ưu hóa: Fil-C đánh đổi hiệu năng để đổi lấy an toàn. Các kỹ thuật tối ưu hóa được sử dụng để giảm thiểu chi phí này.

Khi nào nên sử dụng Fil-C?

Tác giả bài viết gợi ý một số trường hợp sử dụng Fil-C:

  • Khi bạn có một lượng lớn mã C/C++ cũ hoạt động tốt nhưng chưa được chứng minh là an toàn về bộ nhớ, và bạn chấp nhận đánh đổi hiệu năng để có tính bảo mật (có thể như một giải pháp tạm thời trước khi viết lại bằng Rust hay Go).
  • Dùng như công cụ debug (tương tự ASan) để tìm lỗi bộ nhớ.
  • Sử dụng trong môi trường đánh giá tại thời điểm biên dịch (compile-time evaluation) của các ngôn ngữ như Zig.

Fil-C không chỉ là một công cụ bảo mật mà còn là một ví dụ cụ thể về khái niệm "pointer provenance" (nguồn gốc con trỏ), cho thấy việc tối ưu hóa mã của compiler có thể ảnh hưởng thế nào đến ngữ nghĩa của chương trình.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗