← Về danh sách
Bảo mật & Quyền riêng tư
#메모리안전성#버퍼오버플로#Use-after-free#Rust#MTE
Cập nhật lần cuối · 2026-10-23

An toàn bộ nhớ (Memory Safety) và ngôn ngữ an toàn bộ nhớ

1. Tổng quan

Định nghĩa: An toàn bộ nhớ (Memory Safety) là tính chất bảo đảm rằng trong lúc thực thi, chương trình không bao giờ truy cập bộ nhớ vượt ra ngoài giới hạn không gian (spatial) và thời hạn hiệu lực theo thời gian (temporal) của các vùng mà nó đã được cấp phát. Một chương trình an toàn bộ nhớ chỉ đọc và ghi những vùng được cấp phát hợp lệ, chưa được giải phóng, và phù hợp với kiểu của chúng.

C và C++ mang lại hiệu năng cao và khả năng điều khiển phần cứng trực tiếp thông qua số học con trỏ và quản lý bộ nhớ thủ công, nhưng đổi lại người lập trình phải chịu toàn bộ trách nhiệm kiểm tra giới hạn và quản lý vòng đời. Vấn đề là việc con người hoàn thành trách nhiệm này một cách hoàn hảo trên thực tế là bất khả thi. Những sai sót như ghi quá cuối bộ đệm một byte (off-by-one), hay tham chiếu lại một con trỏ tới bộ nhớ đã giải phóng (use-after-free) vẫn sống sót qua rà soát mã và kiểm thử để đến môi trường vận hành. Những khiếm khuyết còn sót như vậy không chỉ là lỗi đơn thuần; chúng trở thành điểm khởi đầu của thực thi mã từ xa (RCE), qua đó kẻ tấn công chiếm quyền luồng điều khiển của chương trình.

Lý do vấn đề này được nâng lên thành nghị trình toàn ngành là vì số liệu thống kê áp đảo. Microsoft cho biết khoảng 70% các lỗ hổng bảo mật (CVE) được công bố mỗi năm trong sản phẩm của họ là vấn đề an toàn bộ nhớ, và Google cũng lặp đi lặp lại xác nhận tỷ lệ tương tự ở Chromium và Android. Nói cách khác, an toàn bộ nhớ không phải một loại lỗ hổng trong vô số loại, mà là nguồn gốc cấu trúc của các lỗ hổng mức độ nghiêm trọng cao. Vì cách vá từng khiếm khuyết dẫn đến tái phát bất tận, lĩnh vực đang chuyển dịch sang cách tiếp cận "loại bỏ lỗ hổng từ gốc (eliminate at the source)".

Khi bàn về an toàn bộ nhớ, có ba đặc điểm cần đặt làm tiền đề. Thứ nhất, nó là bảo đảm nhị phân — chỉ cần một chỗ không an toàn, khe hở đó có thể làm lộ toàn bộ bộ nhớ tiến trình, nên "an toàn 90%" gần như là không an toàn. Thứ hai, có căng thẳng bản nguyên với hiệu năng — việc kiểm tra giới hạn và vòng đời đòi hỏi một chi phí dưới dạng nào đó, nên mỗi giải pháp khác nhau ở chỗ dời chi phí ấy đi đâu (lúc chạy, lúc biên dịch, hay phần cứng). Thứ ba, có sự phụ thuộc di sản lớn — hạ tầng cốt lõi của thế giới được viết bằng C/C++, nên giải pháp phải bao hàm không chỉ "cách làm mới" mà cả "cách gia cố cái sẵn có" mới có tính hiện thực.

An toàn bộ nhớ đặc biệt khó xử lý vì nó là thuộc tính chất lượng bảo mật phi chức năng chứ không phải yêu cầu chức năng. Nó không hiện ra ở những tính năng người dùng thấy nên dễ bị hạ ưu tiên, nhưng một khi bị xuyên thủng thì nó làm sụp đổ độ tin cậy của toàn hệ thống. Lý do an toàn bộ nhớ là một nhiệm vụ nặng nề đến vậy là vì khiếm khuyết của nó nằm im lặng tiềm ẩn. Một lỗi chức năng hiện ra ngay lập tức dưới dạng màn hình sai hay ngoại lệ, nhưng vi phạm an toàn bộ nhớ thường ngày vẫn chạy bình thường và chỉ vỡ dưới một số đầu vào hay thời điểm nhất định. Một thao tác ghi vượt ranh giới một byte làm ô nhiễm vi tế biến lân cận, và hậu quả bùng phát muộn ở một đường mã hoàn toàn khác. Vì tính phi cục bộ (non-locality) này mà việc gỡ lỗi cực kỳ khó khăn và dễ bỏ sót ngay cả khi độ phủ kiểm thử cao. Chính đặc điểm này định ra giới hạn của cách tiếp cận "dựa vào sự cẩn trọng của con người".

Sự chuyển dịch ấy đã được chính thức hóa thành các khuyến nghị cấp chính phủ và tiêu chuẩn trong giai đoạn 2022–2024. Tờ thông tin "Software Memory Safety" của NSA Hoa Kỳ (2022), "The Case for Memory Safe Roadmaps" của CISA, NSA, FBI cùng các đối tác quốc tế (2023), và "Back to the Building Blocks" của Văn phòng Giám đốc An ninh mạng Quốc gia Nhà Trắng (ONCD) (2024) lần lượt khuyến nghị chuyển sang ngôn ngữ an toàn bộ nhớ. Dưới góc nhìn Kỹ sư chuyên nghiệp, bản chất của chủ đề này không phải là "cách bắt lỗi tốt hơn" mà là sự chuyển dịch mô thức thiết kế trên toàn chuỗi cung ứng phần mềm, trải rộng từ ngôn ngữ, runtime, phần cứng đến chính sách tổ chức.

2. Cấu trúc và các loại vi phạm an toàn bộ nhớ

Để hiểu vi phạm an toàn bộ nhớ, trước hết phải chia theo hai trục. An toàn không gian là câu hỏi "có đang truy cập trong phạm vi cho phép không," còn an toàn thời gian là câu hỏi "có đang truy cập tại thời điểm còn hiệu lực không." Nếu tràn bộ đệm là đại diện của trục trước thì use-after-free là đại diện của trục sau. Các loại lỗ hổng thực tế phái sinh từ sự giao nhau của hai trục.

Sự phân biệt này quan trọng vì hai loại an toàn độc lập với nhau, nên chỉ giữ một bên thì không trọn vẹn. Dù kiểm tra giới hạn (không gian) kỹ đến đâu cũng không ngăn được vi phạm thời gian dùng con trỏ đã giải phóng; ngược lại, dù quản lý vòng đời hoàn hảo, vượt phạm vi chỉ số vẫn sinh vi phạm không gian. Do đó bảo đảm "an toàn bộ nhớ" chỉ thành lập khi thỏa cả hai trục đồng thời, và mọi giải pháp phải được đánh giá qua cách nó xử lý cả hai khía cạnh cùng nhau.

flowchart TD
    MS["Vi phạm an toàn bộ nhớ"]
    MS --> SP["Vi phạm không gian (vượt giới hạn)"]
    MS --> TP["Vi phạm thời gian (vi phạm vòng đời)"]
    SP --> OF["Tràn trên/dưới bộ đệm"]
    SP --> OOB["Đọc/ghi ngoài phạm vi (OOB)"]
    SP --> TC["Nhầm kiểu (type confusion)"]
    TP --> UAF["Dùng sau giải phóng (use-after-free)"]
    TP --> DF["Giải phóng kép (double free)"]
    TP --> UI["Đọc chưa khởi tạo (uninitialized read)"]
    OF --> EXP["Chiếm luồng điều khiển / rò rỉ thông tin"]
    OOB --> EXP
    TC --> EXP
    UAF --> EXP
    DF --> EXP
    UI --> EXP

Điểm cốt lõi trong sơ đồ trên là nhiều loại lỗ hổng tưởng chừng khác nhau rốt cuộc hội tụ về một kết quả (kẻ tấn công đọc/ghi tùy ý bộ nhớ). Nói cách khác, kẻ tấn công chỉ cần làm thành công bất kỳ một loại nào trong số đó là nắm được sức tấn công như nhau. Việc phòng thủ chỉ chặn một loại là vô ích; lý do cấu trúc cần một bảo đảm toàn diện niêm phong cả hai trục an toàn nằm ở đây. Vi phạm không gian phát sinh khi không giữ được giới hạn của mảng hay bộ đệm. Ví dụ, sao chép một chuỗi dài vào một bộ đệm ngăn xếp ngắn bằng strcpy mà không kiểm tra độ dài có thể ghi đè địa chỉ trả về lân cận, nhảy đến mã do kẻ tấn công chỉ định khi hàm trả về. Đây là tràn ngăn xếp cổ điển đã tái diễn gần nửa thế kỷ kể từ sâu Morris năm 1988.

Vi phạm không gian đặc biệt nguy hiểm vì dữ liệu và thông tin điều khiển xen lẫn trong cùng một không gian bộ nhớ. Ngăn xếp chứa không chỉ biến cục bộ mà cả địa chỉ trả về của hàm và các thanh ghi được lưu, còn trên heap thì dữ liệu người dùng nằm kề metadata của bộ cấp phát. Khoảnh khắc một thao tác ghi vượt ranh giới chạm vào thông tin điều khiển này, hỏng dữ liệu đơn thuần nhảy vọt thành chiếm đoạt luồng chương trình. Sự thật rằng bố cục bộ nhớ quyết định bề mặt tấn công là điểm khởi đầu của nhiều kỹ thuật giảm thiểu cố gắng ngẫu nhiên hóa hoặc tách rời bố cục.

Vi phạm thời gian đến từ thất bại trong quản lý vòng đời (lifetime). Use-after-free là trường hợp dùng lại một con trỏ (dangling pointer) tới bộ nhớ đã trả về qua free(). Nếu khối đã giải phóng được cấp phát lại cho đối tượng khác rồi bị truy cập qua con trỏ cũ, kẻ tấn công có thể thao túng nội dung của một đối tượng bằng con trỏ của đối tượng khác. Đây là lý do use-after-free đặc biệt chí mạng ở các chương trình quy mô lớn như trình duyệt web nơi vòng đời đối tượng đan xen phức tạp. Bảng dưới đây tóm tắt các loại chính cùng nguyên nhân và ảnh hưởng điển hình.

Loại Phân lớp Nguyên nhân điển hình Ảnh hưởng chính
Tràn bộ đệm ngăn xếp/heap Không gian Thiếu kiểm tra độ dài, off-by-one Chiếm luồng điều khiển, RCE
Đọc ngoài phạm vi (OOB read) Không gian Thiếu kiểm định chỉ số Rò rỉ thông tin (vd: Heartbleed)
Dùng sau giải phóng (UAF) Thời gian Tham chiếu lại dangling pointer Thao túng đối tượng, RCE
Giải phóng kép (double free) Thời gian free trùng, nhầm quyền sở hữu Hỏng metadata heap
Nhầm kiểu Không gian/kiểu Ép kiểu sai Đọc/ghi tùy ý
Dùng bộ nhớ chưa khởi tạo Thời gian Thiếu khởi tạo Rò rỉ thông tin, hành vi phi tất định

Như bảng cho thấy, dạng thức ảnh hưởng khác nhau nhưng gốc rễ như nhau. Tất cả đều bắt nguồn từ việc "con trỏ nhìn vào bộ nhớ lệch pha với giới hạn hay vòng đời của đối tượng thực tế." Do đó biện pháp căn cơ cũng hội tụ — không phải vá theo từng loại mà theo hướng để ngôn ngữ hoặc phần cứng cưỡng chế sự khớp nhau giữa con trỏ và đối tượng.

Mặt khác, việc dùng bộ nhớ chưa khởi tạo ít nổi bật hơn nhưng là lối rò rỉ thông tin lặng lẽ. Nếu một bộ đệm chỉ được cấp phát mà không khởi tạo được gửi ra ngoài nguyên trạng, dữ liệu khác từng chiếm chỗ đó (vd: khóa mật mã còn sót) có thể rò ra cùng. Cũng là đặc điểm khi mà, do phi tất định, nó khó tái hiện trong kiểm thử.

Rằng các loại này không phải rủi ro trừu tượng được chứng minh bởi các sự cố lớn thực tế. Năm 2014, Heartbleed của OpenSSL (CVE-2014-0160) chỉ với một thao tác đọc ngoài phạm vi (OOB read) không kiểm định trường độ dài đã làm rò rỉ cả khóa riêng và phiên từ bộ nhớ máy chủ, kích hoạt việc cấp lại hạ tầng HTTPS trên toàn cầu. Năm 2017, EternalBlue của Windows SMB khai thác một khiếm khuyết thuộc lớp tràn bộ đệm để trở thành đường dẫn cho đại dịch mã độc tống tiền WannaCry và NotPetya. Cả hai sự cố đều là trường hợp "một sai sót ranh giới trong một dòng bộ nhớ" bị khuếch đại thành thiệt hại quy mô quốc gia và ngành, cho thấy vì sao an toàn bộ nhớ phải được xử lý không phải như một lỗi đơn lẻ mà như một rủi ro hệ thống.

3. Cơ chế khai thác và kỹ thuật giảm thiểu

Một khiếm khuyết bộ nhớ không ngay lập tức nghĩa là một cuộc tấn công thành công. Giữa lỗ hổng (vulnerability) và khai thác thực tế (exploit) có nhiều giai đoạn, và phòng thủ dựng rào chắn ở giữa. Kẻ tấn công dùng khiếm khuyết để (1) ghi đè mã hoặc con trỏ, (2) vòng qua các kỹ thuật phòng thủ, và (3) thực thi hành vi độc hại thực sự; bên phòng thủ đặt các kỹ thuật giảm thiểu ở mỗi giai đoạn để nâng chi phí tấn công. Góc nhìn "nâng chi phí" này là khung cốt lõi giải thích vì sao chỉ giảm thiểu là chưa đủ. Dưới đây là kiến trúc chi tiết thể hiện một chuỗi khai thác điển hình và các tầng giảm thiểu tương ứng.

flowchart LR
    subgraph ATK["Chuỗi khai thác"]
        V["Phát hiện khiếm khuyết bộ nhớ"] --> OW["Đạt ghi tùy ý bộ nhớ"]
        OW --> HJ["Chiếm luồng điều khiển"]
        HJ --> PAY["Thực thi payload (ROP / shellcode)"]
    end
    subgraph DEF["Tầng giảm thiểu (phòng thủ nhiều lớp)"]
        C1["Stack canary"]
        C2["DEP / NX (chống thực thi)"]
        C3["ASLR (ngẫu nhiên hóa địa chỉ)"]
        C4["CFI (toàn vẹn luồng điều khiển)"]
    end
    OW -. "phát hiện" .-> C1
    PAY -. "chặn" .-> C2
    HJ -. "gây nhiễu" .-> C3
    HJ -. "kiểm chứng" .-> C4

Ví dụ, trong một tràn ngăn xếp điển hình, một thao tác sao chép không kiểm tra độ dài ghi 128 byte vào bộ đệm 64 byte, ghi đè địa chỉ trả về được lưu kề bên, và khoảnh khắc hàm trả về thì nó rẽ nhánh đến địa chỉ kẻ tấn công đã cài. Các kỹ thuật giảm thiểu cố gắng cắt từng mắt xích của chuỗi này. Trong chuỗi này, các kỹ thuật giảm thiểu là những rào chắn xác suất không chặn cuộc tấn công từ gốc mà nâng chi phí của nó. Stack canary đặt một giá trị ngẫu nhiên trước địa chỉ trả về để nếu tràn làm đổi giá trị ấy thì dừng thực thi trước khi hàm trả về. DEP/NX đánh dấu các vùng dữ liệu là không thể thực thi để chặn việc chạy trực tiếp shellcode tiêm vào ngăn xếp. Khi đó kẻ tấn công vòng qua bằng ROP (Return-Oriented Programming), nối các mẩu mã sẵn có (gadget), và đáp lại thì ASLR ngẫu nhiên hóa địa chỉ để khó dự đoán vị trí gadget.

Vấn đề là cuộc giằng co này không có hồi kết. ASLR bị vô hiệu khi địa chỉ rò qua rò rỉ thông tin (OOB read), còn CFI không trọn vẹn do chi phí hiệu năng và kỹ thuật vòng qua. Tức là các kỹ thuật giảm thiểu mang tính hậu nghiệm và xác suất, nên một khi kẻ tấn công nắm được một nguyên thủy đọc/ghi tùy ý mạnh, chúng có thể sụp đổ tuần tự. Thực tế, ở các cuộc thi hack như Pwn2Own, ngay cả các trình duyệt mới nhất trang bị đủ các giảm thiểu này cũng bị xuyên thủng lặp đi lặp lại bởi các chuỗi khai thác khởi đầu từ một khiếm khuyết bộ nhớ. Giới hạn này biện minh cho việc chuyển sang các cách tiếp cận cấp ngôn ngữ và phần cứng "không tạo ra khiếm khuyết ngay từ đầu".

Mặt khác, các kỹ thuật kiểm thử phát hiện khiếm khuyết từ trước cũng là một trục của hệ thống giảm thiểu. Fuzzing dựa trên độ phủ tiêm một lượng lớn đầu vào ngẫu nhiên/đột biến để dò các đường bất thường, và khi kết hợp với một bộ phân tích động như AddressSanitizer (ASan) thì tràn và UAF hiện ra ngay lập tức dưới dạng sập thực tế. OSS-Fuzz của Google đã tự động tìm ra hàng chục nghìn khiếm khuyết trong các dự án mã nguồn mở lớn theo cách này. Tuy nhiên, fuzzing chỉ tìm được khiếm khuyết trên những đường nó chạm tới, nên không thể bảo đảm tính đầy đủ — việc kiểm thử có thể giảm khiếm khuyết nhưng không thể chứng minh sự "vắng mặt" của chúng chính là lý do việc chuyển sang ngôn ngữ an toàn là phương thuốc căn cơ chứ không phải phần bổ sung.

4. So sánh các cách tiếp cận bảo đảm an toàn bộ nhớ

Nếu các giảm thiểu nêu trên làm cho tấn công "khó" thì cách tiếp cận bảo đảm an toàn nhắm tới làm cho chính khiếm khuyết "bất khả thi". Có ba con đường lớn để đạt an toàn bộ nhớ. Thứ nhất là ngôn ngữ an toàn kiểu kiểm tra lúc chạy, nơi mà, như Java, C#, Go và Python, một bộ thu gom rác (GC) tự động quản lý vòng đời và chèn kiểm tra giới hạn ở mọi truy cập mảng. Năng suất và an toàn cao, nhưng có độ trễ (latency) và chi phí bộ nhớ do GC gây ra, nên có thể không phù hợp với nhân hệ điều hành hay hệ thống thời gian thực. GC loại bỏ dangling pointer từ gốc bằng cách để runtime theo dõi "khi nào thu hồi bộ nhớ," nhưng chi phí theo dõi đó và việc dừng thu gom khó lường (stop-the-world) là đánh đổi. Đây là lý do việc tinh chỉnh GC trở thành nhiệm vụ thường trực ở những lĩnh vực mà độ trễ đuôi (tail latency) quan trọng, như giao dịch tài chính hay máy chủ trò chơi.

Thứ hai là Rust, một ngôn ngữ an toàn kiểu kiểm chứng lúc biên dịch. Không cần GC, Rust dùng một bộ kiểm tra mượn (borrow checker) trong đó trình biên dịch kiểm chứng tĩnh các quy tắc về quyền sở hữu (ownership), mượn (borrowing) và vòng đời (lifetime), cưỡng chế rằng mỗi giá trị có đúng một chủ và một tham chiếu khả biến không thể đồng tồn tại cùng các tham chiếu bất biến. Điều này chặn use-after-free và tranh chấp dữ liệu (data race) từ gốc mà không có chi phí lúc chạy. Dẫu vậy, mã cấp thấp buộc phải bước ra ngoài mô hình sở hữu được tách riêng tường minh thành các khối unsafe để thu hẹp trách nhiệm kiểm chứng.

An toàn của Rust nằm ở chỗ "biến điều bất khả thi thành lỗi biên dịch". Ví dụ, mã cố gắng sửa đổi một tập hợp trong khi đang duyệt nó — mã mà ở các ngôn ngữ khác sẽ dẫn đến vô hiệu hóa bộ lặp (iterator invalidation) lúc chạy — thì ở Rust không biên dịch được vì vi phạm quy tắc cấm đồng tồn tại tham chiếu khả biến và bất biến. Vì khiếm khuyết nhờ đó bị lọc bỏ ở giai đoạn phát triển trước khi đến sản xuất, trách nhiệm an toàn dời từ sự cẩn trọng của con người sang sự bảo đảm của công cụ. Đây là lý do Rust được gọi là "zero-cost abstraction" — nó trả giá cho an toàn không phải bằng hiệu năng lúc chạy mà bằng thời gian biên dịch và ràng buộc thiết kế.

Thứ ba là an toàn có hỗ trợ phần cứng. Là cách tiếp cận gia cố an toàn mà không viết lại các tài sản C/C++ sẵn có, Memory Tagging Extension (MTE) của Arm và CHERI/Morello là tiêu biểu. Bảng dưới đây so sánh ba cách tiếp cận theo các tiêu chí.

Tiêu chí Kiểm tra lúc chạy (Java/Go) Lúc biên dịch (Rust) Hỗ trợ phần cứng (MTE/CHERI)
Thời điểm bảo đảm an toàn Khi thực thi Khi biên dịch Khi thực thi (HW cưỡng chế)
Chi phí hiệu năng Chi phí GC / kiểm tra Gần như không Thấp–trung (tùy thiết kế)
Tái dùng C/C++ sẵn có Khó (viết lại) Khó (viết lại) Dễ (biên dịch lại / gia cố)
Chi phí học / chuyển đổi Thấp–trung Cao (sở hữu) Trung (toolchain / HW)
Lĩnh vực áp dụng Ứng dụng / máy chủ Hệ thống / nhân / ứng dụng Gia cố di sản

Điều quan trọng trong so sánh của bảng là lý do khác biệt phát sinh. Rust gần như không có chi phí hiệu năng vì nó dời các kiểm tra an toàn từ lúc chạy sang thời điểm biên dịch, và đổi lại chi phí học của người phát triển khi làm chủ các quy tắc sở hữu là cao. Ngược lại, cách tiếp cận hỗ trợ phần cứng có lợi cho gia cố di sản vì nó có thể gắn thẻ vào con trỏ và kiểm chứng chỉ bằng biên dịch lại mà không viết lại mã nguồn, và đổi lại nó cần tính năng CPU chuyên dụng và chi phí lưu thẻ bộ nhớ. Thay vì một cái nào đó vượt trội tuyệt đối, giải pháp hiện thực là kết hợp chúng một cách chiến lược tùy theo ngân sách hiệu năng, quy mô di sản và năng lực đội ngũ của hệ thống mục tiêu.

Trong thực tế, các cách tiếp cận được pha trộn ngay cả trong một hệ thống. Ví dụ, lõi mật mã/mạng chí mạng về hiệu năng có thể viết lại bằng Rust, một thư viện C di sản đồ sộ được gia cố bằng MTE, và logic nghiệp vụ lớp trên để ở một ngôn ngữ GC — một áp dụng khác biệt theo tầng. Điểm mấu chốt ở đây là ranh giới (FFI, foreign function interface) nơi các mô-đun có bảo đảm an toàn khác nhau tiếp giáp. Khoảnh khắc một ngôn ngữ an toàn gọi mã không an toàn, bảo đảm bị cắt đứt, nên việc tối thiểu hóa ranh giới và tập trung kiểm chứng, lập tài liệu cho điểm đó chi phối an toàn tổng thể.

5. Chuyên sâu: Xu hướng chính sách, phần cứng và áp dụng công nghiệp

An toàn bộ nhớ không còn là vấn đề chất lượng nội bộ trong các đội phát triển mà đã trở thành một trục chuyển dịch hệ sinh thái mà chính sách, phần cứng và công nghiệp cùng vận động. Trong ba năm qua, an toàn bộ nhớ đã bước vào lĩnh vực chính sách và quy định quốc gia vượt ra ngoài diễn ngôn kỹ thuật. Báo cáo "Back to the Building Blocks" do ONCD Nhà Trắng công bố tháng 2 năm 2024 khuyến nghị các nhà sản xuất phần mềm áp dụng ngôn ngữ an toàn bộ nhớ để "loại bỏ toàn bộ các lớp lỗ hổng," đưa C và C++ làm ví dụ về ngôn ngữ không an toàn bộ nhớ và Rust làm ví dụ về ngôn ngữ an toàn. Điều này nối với dòng chảy trong đó "The Case for Memory Safe Roadmaps" (2023) trước đó của CISA, NSA, FBI cùng các đối tác quốc tế yêu cầu nhà sản xuất thiết lập một lộ trình an toàn bộ nhớ cho các sản phẩm sẵn có. "Exploring Memory Safety in Critical Open Source Projects," do CISA và các bên công bố tháng 6 năm 2024, phân tích 172 dự án mã nguồn mở trọng yếu và chỉ ra rằng đa số vẫn được viết bằng C/C++ và rằng ngay cả các dự án viết bằng ngôn ngữ an toàn cũng có thể bị phơi nhiễm qua các phụ thuộc không an toàn. (Mức độ thực thi/cưỡng chế cụ thể có thể thay đổi, nên cần tham khảo bản gốc.)

Xu hướng về phía phần cứng cũng rõ ràng. Memory Tagging Extension (MTE) của Arm là một cơ chế "khóa và chìa" chia bộ nhớ vật lý thành các đơn vị 16 byte (granule), gán cho mỗi đơn vị một thẻ 4 bit, nạp thẻ cùng loại lên con trỏ, và để phần cứng kiểm tra khớp thẻ khi truy cập. Nếu thẻ lệch nhau, nó phát hiện ngay vi phạm không gian/thời gian. Google báo cáo rằng họ đã bắt được một số lượng đáng kể lỗi bộ nhớ trước khi phát hành nhờ tận dụng MTE trong Android (con số từ phía nhà cung cấp, nên cần thận trọng khi diễn giải). Cách tiếp cận căn cơ hơn, CHERI, được đánh giá trên nền thử nghiệm Morello của Arm, thay thế con trỏ thông thường bằng các năng lực (capability) phần cứng không thể giả mạo để khắc giới hạn và quyền vào chính con trỏ.

Việc gia cố phần cứng này đóng vai trò lấp khoảng trống của chuyển dịch phần mềm. Viết lại bằng ngôn ngữ an toàn mất nhiều năm, nhưng trong khoảng thời gian đó di sản vẫn phơi ra trước tấn công. Một tính năng phần cứng như MTE áp dụng trực tiếp vào các tệp nhị phân sẵn có để giảm thiểu "rủi ro giai đoạn chuyển tiếp" này, và theo nghĩa đó nó bổ trợ cho cách tiếp cận phần mềm theo trục thời gian chứ không cạnh tranh với nó.

Sự khác biệt triết lý giữa hai cách tiếp cận phần cứng cũng đáng ngẫm. MTE mang tính xác suất và tiệm tiến — một thẻ 4 bit chỉ có 16 khả năng, nên vẫn còn xác suất khớp ngẫu nhiên, nhưng nó có thể áp dụng rộng chỉ bằng biên dịch lại mà không sửa mã nguồn, phù hợp mục tiêu thực dụng "gia cố di sản ngay bây giờ". Ngược lại, CHERI mang tính tất định và căn cơ — nó niêm phong giới hạn từ gốc bằng các năng lực, nhưng nó thay đổi ABI và biểu diễn con trỏ, đòi hỏi thiết kế lại trên toàn hệ sinh thái. Sự tương phản này cho thấy bảo mật luôn là một cân bằng giữa "tính trọn vẹn" và "khả năng chấp nhận", gợi ý rằng trước mắt hai cách tiếp cận có thể cùng tồn tại một cách bổ trợ.

Các trường hợp áp dụng công nghiệp cho thấy một chiến lược "an toàn từ mã mới" thay vì "viết lại tất cả". Google, sau khi chuyển sang chính sách áp dụng Rust trước cho mã viết mới trong Android, công bố rằng tỷ lệ lỗ hổng an toàn bộ nhớ trong mã mới giảm đáng kể. Nhận thức then chốt là vì lỗ hổng tập trung phần lớn ở mã mới và các thay đổi gần đây, người ta có thể bẻ cong đường cong lỗ hổng tổng thể chỉ bằng cách đổi ngôn ngữ của mã mới, mà không cần chuyển toàn bộ di sản hàng chục năm tuổi. Microsoft (đưa Rust vào các phần của nhân Windows), AWS (Firecracker, v.v.), và nhân Linux (Rust for Linux) cũng đang đi theo cùng con đường chuyển dịch tiệm tiến.

Chiến lược này hiệu quả vì một lý do định lượng. Mật độ khiếm khuyết có xu hướng tỷ lệ nghịch với "tuổi" của mã, nên mã cũ, ổn định đã được lọc bỏ vô số lỗi, trong khi khiếm khuyết tập trung ở mã mới thêm vào. Do đó, áp dụng ngôn ngữ an toàn chỉ cho mã mới vẫn làm giảm cả tỷ lệ mã không an toàn trong toàn bộ cơ sở mã lẫn lỗ hổng mới theo thời gian. Nhận thức này — rằng có thể bẻ cong đường cong rủi ro mà không cần mục tiêu phi hiện thực là viết lại toàn bộ — là nền tảng lý thuyết của một chiến lược chuyển dịch thực dụng.

Trong bối cảnh nội địa (Hàn Quốc), xu hướng này cũng đang dần được phản ánh. Một phần lớn các hệ thống cốt lõi trong khu vực công và tài chính gồm di sản dựa trên C/C++ và phần sụn nhúng, nên cùng với việc tăng cường an ninh chuỗi cung ứng phần mềm (bắt buộc SBOM, chẩn đoán điểm yếu bảo mật, v.v.), an toàn bộ nhớ đang nổi lên như một chỉ số chất lượng/bảo mật trung–dài hạn. Tuy nhiên, vì chuyển sang ngôn ngữ an toàn phải được hậu thuẫn bởi trình độ nhân lực và hệ thống kiểm chứng, các tổ chức nội địa nên ưu tiên áp dụng chọn lọc cho mô-đun mới / thành phần rủi ro cao và nội tại hóa phân tích tĩnh cùng fuzzing hơn là chuyển đổi toàn diện.

6. Cân nhắc và hàm ý

An toàn bộ nhớ không phải nhiệm vụ kết thúc bằng việc đưa vào một công cụ duy nhất mà, dưới góc nhìn Kỹ sư chuyên nghiệp, là một nghị trình chiến lược đòi hỏi một lộ trình chuyển dịch nhiều năm và quản lý đánh đổi. Các cân nhắc dưới đây mở rộng ra ngoài lựa chọn công nghệ sang chiến lược đầu tư, quản trị và chuỗi cung ứng của tổ chức.

  • Chiến lược chuyển dịch tiệm tiến (tránh viết lại toàn bộ): Chuyển hàng triệu dòng di sản cùng lúc có chi phí và rủi ro quá mức. Con đường hiện thực là một cách tiếp cận strangler viết các mô-đun mới và nhiều thay đổi trước bằng một ngôn ngữ an toàn như Rust trong khi quản lý rõ ràng các điểm ranh giới (FFI). Tổ chức nên đặt lộ trình an toàn bộ nhớ và KPI theo từng sản phẩm (vd: tỷ lệ ngôn ngữ an toàn trong mã mới).
  • Quản lý định lượng đánh đổi hiệu năng/năng suất: Độ trễ của ngôn ngữ GC, đường cong học của Rust và chi phí thẻ của cách tiếp cận phần cứng mỗi cái áp đặt những chi phí khác nhau. Nên áp dụng khác biệt các cách tiếp cận bằng cách phân biệt các vùng nhân/thời gian thực nhạy độ trễ, ứng dụng nghiệp vụ nơi năng suất quan trọng, và các vùng mà gia cố di sản cấp bách, rồi ra quyết định bằng cách định lượng chi phí qua đo chuẩn.
  • Mở rộng từ góc nhìn chuỗi cung ứng/phụ thuộc: Dù mã của chính mình được chuyển sang ngôn ngữ an toàn, một khiếm khuyết trong thư viện hay phụ thuộc C/C++ vẫn để lại phơi nhiễm. Một chiến lược phải tiến hành song song để hiển thị ngôn ngữ/an toàn của các phụ thuộc qua SBOM (bảng kê vật tư phần mềm) và cô lập các phụ thuộc không an toàn bằng binding ngôn ngữ an toàn hoặc sandbox hóa.
  • Dùng song song với phòng thủ nhiều lớp (trước khi chuyển đổi hoàn toàn): Vì chuyển đổi cần thời gian, trong giai đoạn đó phải chạy song song các kỹ thuật giảm thiểu như ASLR, CFI, stack canary và MTE cùng với kiểm thử dựa trên fuzzing, phân tích tĩnh (SAST) và AddressSanitizer để quản lý rủi ro còn sót. Việc áp dụng một ngôn ngữ an toàn không thay thế các kỹ thuật giảm thiểu mà giảm gánh nặng của chúng từ gốc.
  • Năng lực nhân lực/tổ chức và ứng phó quy định: Một mô thức mới như mô hình sở hữu kéo theo chi phí đào tạo/tuyển dụng. Đồng thời, vì mua sắm chính phủ và chứng nhận bảo mật ngày càng yêu cầu lộ trình an toàn bộ nhớ, ứng phó quy định và nội tại hóa năng lực nên được gói thành nhiệm vụ đồng thời và phản ánh vào kế hoạch trung–dài hạn.
  • Mục tiêu đo lường được và quản trị: Chỉ tuyên bố "chúng tôi nhắm tới an toàn bộ nhớ" thôi thì không thúc đẩy được chuyển dịch. Như báo cáo ONCD nhấn mạnh, tính đo lường được (measurability) là then chốt, và chỉ khi các chỉ số như tỷ lệ ngôn ngữ an toàn trong mã mới, xu hướng CVE an toàn bộ nhớ, và số lượng ranh giới an toàn/không an toàn (FFI) được định nghĩa và đưa vào hệ thống báo cáo quản lý thì chuyển dịch mới được theo dõi và quản lý ở cấp độ tổ chức.
  • Lưu ý trong thời đại sinh mã bằng AI: Trong môi trường mà AI tạo sinh tuôn ra mã hàng loạt, có rủi ro rằng các mẫu không an toàn bị tái sản xuất vì vô số thành ngữ C/C++ có trong dữ liệu huấn luyện. Mã do AI tạo ra cũng nên được đặt dưới cùng một sự kiểm soát về phân tích tĩnh, fuzzing và chính sách ngôn ngữ, sao cho các lan can an toàn được thiết kế để ngăn lợi ích năng suất biến thành thoái lui an toàn.

Tài liệu tham khảo


Tóm tắt một câu: An toàn bộ nhớ là tính chất bảo đảm con trỏ không bao giờ lệch pha với giới hạn và vòng đời của đối tượng; đây là vấn đề gốc rễ đứng sau khoảng 70% các lỗ hổng nghiêm trọng, và chiến lược then chốt là một chuyển dịch tiệm tiến kết hợp kiểm tra lúc chạy (Java/Go), kiểm chứng lúc biên dịch (Rust) và hỗ trợ phần cứng (MTE/CHERI) cho phù hợp tình huống.