
CVE-2026-46331(昵称 “pedit COW”)是 Linux 流量控制子系统中一个本地内核权限提升漏洞。无特权用户(在无特权网络命名空间中)可以配置 act_pedit(包编辑器)过滤器,触发对页缓存的 部分写时复制(COW)写入。实际上,内核将攻击者控制的数据写入文件的内存映像而无需将页面标记为私有,从而损坏该文件的缓存副本。至关重要的是,该漏洞仅需 CAP_NET_ADMIN(可在用户命名空间中获取),且不会修改磁盘上的文件。实践中,一个名为 packet_edit_meme 的有效概念验证(PoC)已于 2026 年 6 月 17 日发布,演示了如何覆盖 setuid 二进制文件(例如 /bin/su)的页缓存映像以获取 root shell。该漏洞源于 tcf_pedit_act() 中错误的 COW 范围计算,并已通过将可写区域检查移至每个键的循环内得到上游修复(2026 年 6 月 4 日)。
act_pedit 的 Linux 内核(约 v5.18 至 7.1-rc6)。未打补丁的稳定版本(包括许多发行版内核)存在漏洞。tc pedit 过滤器,并在内存中用 shellcode 覆盖 setuid 二进制文件的 ELF 入口点。skb_ensure_writable() 移入键循环内)。作为权宜之计,阻止或卸载 act_pedit 模块,或禁用无特权用户命名空间(例如 sysctl user.max_user_namespaces=0)。缓解后,丢弃缓存(echo 3 > /proc/sys/vm/drop_caches)以驱逐任何被投毒的页面。本报告提供 CVE-2026-46331 的详细技术分析:其原因、利用、检测和修复策略,并附有供应商公告、CVE 及公开漏洞的参考信息。
定义: CVE-2026-46331 是 Linux 内核 流量控制(net/sched)子系统中的一个越界写入漏洞,具体位于 act_pedit(包编辑器)动作中。函数 tcf_pedit_act() 在遍历类型化键之前,使用静态提示 tcfp_off_max_hint 为包编辑操作计算一个“写时复制”范围。然而,某些键(例如 TCP/UDP 头部编辑)仅在运行时确定其最终字节偏移量。该代码从未针对这些动态偏移量重新检查可写性。结果,写入可能发生在预 COW 区域之外:包写入的一部分从未被设置为私有,导致 部分 COW。如果包缓冲区恰好引用了文件页面,则此错误写入会传播到文件的共享页缓存内存中,从而损坏缓存的文件映像。
背景: Linux 包编辑器(pedit) 动作允许管理员在数据包通过配置的 tc 过滤器时重写包头部中的任意字节。它通过指定一个偏移量(可能锚定到某个头部)和一个 32 位值/掩码来工作。内部,pedit 操作套接字缓冲区(sk_buff),并且必须在修改之前使目标包内存可写(通过 skb_ensure_writable() 以 COW 方式)。理想情况下,内核应在写入前克隆(私有复制)任何共享页面,以避免篡改其他地方使用的内存。
根本原因: 在 tcf_pedit_act() 中,代码错误地只提前计算一次可写范围,使用 tcfp_off_max_hint(最大静态偏移量)。此提示不包含类型化键在包处理过程中添加的任何 运行时头部偏移量。像 TCP 或 UDP 这样的键可以根据运行时 IP 头部的位置计算偏移量(例如,如果前面的键移动了网络头部)。因此,在逐键循环期间,某个键的实际偏移量可能超过预分配为可写的范围。代码随后通过 skb_store_bits() 写入包内存,但由于预 COW 区域之外的页面并未被设置为私有,该写入会破坏仍与页缓存共享的页面。简而言之,“过早计算可写包范围” 导致了越界、跨页写入。负偏移量(例如在入口处编辑以太网头部)也被错误处理,甚至 offset_valid() 也缺少对 INT_MIN 的防护,进一步加剧了缺陷。
为何发生: 此漏洞本质上是写时复制范围计算中的逻辑错误。内核假设 静态 最大偏移量(在加载时已知)足以应对所有编辑。它未能在实际应用具有动态偏移量的键时更新 COW 范围。经过一系列排队编辑后,最终写入可能位于预检查区域之外。由于包缓冲区可能引用内存映射的文件页面(例如,通过零拷贝机制),这种“部分 COW”写入可能到达磁盘上文件的页缓存。实践中,包编辑器动作可能接收来自 sendfile 或 splice 的页面;因此,单个包过滤器操作可以间接地将攻击者选择的数据写入文件的内存映像,而无需修改磁盘。
组件与数据流: 漏洞代码位于 Linux net/sched 子系统(act_pedit.c)中。当数据包匹配已配置的 pedit 规则时,会调用 tcf_pedit_act()。内部它仅调用一次 skb_ensure_writable(skb, X),其中 X = tcfp_off_max_hint。这使得包的前 X 字节变为私有(COW)。然后,在每个 键(编辑操作)的循环中,它通过将 运行时头部偏移量 添加到键的指定偏移量来计算键的实际写入偏移量,并将一个 32 位值写入包中。伪代码如下:```c
u32 off_max = action->tcfp_off_max_hint;
skb_ensure_writable(skb, off_max);
for (i = 0; i < num_keys; i++) {
u32 hdr_off = compute_header_offset(skb, key[i].hdr_type);
u32 write_off = hdr_off + key[i].offset;
skb_store_bits(skb, write_off, &key[i].value, 4);
}
因为 `hdr_off` 只在处理每个键时计算,初始的 `skb_ensure_writable()` 调用没有考虑它。如果 `hdr_off + key[i].offset` 超过 `off_max`,代码会回退到在分片而非主线性区域上调用 `skb_store_bits()`,这意味着它写入了一个未设为私有的页面。这就是失败点。
**攻击面:** 唯一需要的接口是带有 `pedit` 动作的 **tc filter**,这通常需要 **CAP_NET_ADMIN** 能力。然而,普通用户可以在私有网络命名空间(用户命名空间克隆)内获得 CAP_NET_ADMIN,而无需真实权限。因此,无特权用户可以进入用户+网络命名空间,并在 loopback 上创建 `tc pedit` 规则。当数据包被处理时发生写入(攻击者通常在 loopback 上生成流量以触发它)。信任边界(用户与内核)被跨越,因为内核信任自己的 COW 设置,但用户提供的偏移量打破了这一假设。
**内部机制:** 在内核侧,该漏洞表现为**越界写入**(CWE-787)。它破坏映射到用户空间的内核内存(文件页缓存)。具体来说,它可以覆盖恰好在套接字缓冲区中映射的任何文件页的内容。在概念验证中,`/bin/su` 通过被发送到套接字缓冲区而 mmapped,因此漏洞利用翻转其在内存中的入口点字节。这不会修改磁盘上的文件,但该二进制文件的任何后续执行都会从缓存中读取中毒的映像。博客分析指出:
> “因为 skb 可以引用通过 sendfile 引入的零拷贝页,该越界写入可能落在支持真实文件的共享页缓存内存中。内核认为它已经使数据包内存可安全修改;但实际上,随后的写入超出了它实际私有化的区域。”
**信任边界:** 内核错误地认为 `skb_ensure_writable()`(快速路径 COW)会保证所有后续写入的安全性。它没有为每个键重新检查。用户仅控制包过滤器配置和包内容;内核(通过网络命名空间)授予了这一点。一旦该信任被破坏,写入就会逃逸到本应受到保护的文件后备内存中。
## 根因分析
根因是 **pedit 动作中错误的 COW 范围计算**。在代码层面,一次 `skb_ensure_writable()` 调用基于 `tcfp_off_max_hint` 的长度被调用,然后在循环内部实际偏移量可能超过这个值。一个小补丁(2026 年 5 月)通过将 `skb_ensure_writable()` *移动到*循环内部(在知道真实偏移量之后)并添加对负偏移量的检查和特殊处理来修复。换句话说:
- **有缺陷的代码:** ```c
skb_ensure_writable(skb, action->tcfp_off_max_hint);
for each key:
// compute offset (hdr_off + key_offset)
skb_store_bits(skb, write_off, ...);
另外,该修复确保对于负偏移(以太网头部编辑)使用 skb_cow() 处理头部空间,并防范 INT_MIN 情况。提交信息(stack.watch 摘要)指出:“通过将 skb_ensure_writable() 移到实际写入偏移已知的每个键循环内部,并添加偏移算术的溢出检查来修复。”
因此,其存在原因:在代码审查或设计期间,针对每个键的重新计算被忽略了。静态提示优化绕过了每个键需要重新评估的需求。这看似是一个诚实的错误而非恶意疏忽,但其影响严重,因为它违反了 COW 假设。正如 TuxCare 所述,此漏洞是在“常规数据损坏”修复的伪装下合并的,没有附带安全背景。
该漏洞由内核提交 8b796475fd78(2022年5月)引入,直到2026年初才被发现。据消息来源,修复(提交 899ee91156e5,2026年5月31日)作为普通数据损坏补丁提交到 netdev 邮件列表。内核维护者于2026年6月4日合并了该修复(net-7.1-rc7)。仅在2026年6月16日,CVE-2026-46331 才正式分配(大约在补丁出现两周后)。2026年6月17日出现了完全武器化的公开漏洞(packet_edit_meme PoC)。
实际操作顺序为:
多方通过公开补丁注意到了该漏洞。例如,网络安全研究员 Massimiliano Oldani 在随后不久发布了一份详细的技术分析和漏洞利用,指出*“CVE 分配后24小时内,一个名为 packet_edit_meme 的公开工作概念验证漏洞出现在 GitHub 上”*。CloudLinux、TuxCare 和 SentinelOne 在 PoC 公开及 CVE 分配后发布了分析。Debian 安全跟踪器和 PT DBugs 也总结了该问题和可用的公告(见参考文献)。
实际攻击所需的前提条件极少:
攻击者能力: 目标机器上的本地非特权用户。该用户必须能够创建带有网络命名空间的新用户命名空间(通过 unshare(CLONE_NEWUSER|CLONE_NEWNET)),这会在该命名空间内授予 CAP_NET_ADMIN 而无需实际 root 权限。非特权用户命名空间在许多内核(如 RHEL、Debian)上默认启用,在 Ubuntu 上可通过 aa-exec 变通方法重新启用。
目标条件: 目标必须运行易受攻击的 Linux 内核(约 5.18–7.1-rc6),且 act_pedit 模块可用。如果 act_pedit 是内置或已加载,则立即可利用。如果它是模块,则在配置 tc pedit 规则时自动加载。目标不应已应用上游补丁。值得注意的是,攻击者无需对任何文件具有写权限;漏洞通过数据包过滤器写入文件。
攻击链:
unshare --map-root-user --net --pid bash 的命令,创建新的用户+网络命名空间。这在该命名空间中授予 CAP_NET_ADMIN(用户映射为内部的 root)。ifconfig lo up),并可选择启动监听器(例如 nc -l 127.0.0.1 9999)。这为 TC 操作提供了数据包流。tc 命令或 netlink,攻击者在 lo 上创建 qdisc 和过滤器,匹配所有数据包(例如 match u32 0 0),并附加一个包含特制键的 pedit 操作。每个键具有动态头部类型(例如用于 L4 偏移的 IP 头部)和一个偏移,使得实际写入位置(头部起始 + 偏移)刚好超出 skb_ensure_writable() 覆盖的范围。echo '' > /dev/udp/127.0.0.1/53)触发过滤器。内核调用 tcf_pedit_act(),分配一个 COW 范围,然后迭代这些键。至少有一个键的写入落在预 COW 区域之外,导致写入进入共享的页缓存。sendfile 或类似方式将 /bin/su mmap 到套接字中,从而使数据包缓冲区引用该文件的页面。越界写入随后损坏了内存中的 /bin/su 副本(特别是 ELF 入口点)。/bin/su)。由于内核无意中注入了执行 setgid(0); setuid(0); execve("/bin/sh") 的 shellcode,运行 su 会生成一个 root shell。磁盘上的文件从未被更改,因此磁盘文件完整性工具不会显示变化。影响: 如果成功,攻击者在本地获得完全 root 权限。该漏洞可以通过单条命令完成,且具有确定性。此外,如果以不同方式使用,对任意文件映射页的损坏可能导致拒绝服务(系统崩溃)。公开的 PoC 专门覆盖了 /bin/su 的入口点以插入 shellcode,但攻击者可以针对任何可映射的文件。该利用链不需要特定时机或竞态条件,并已在多个发行版(RHEL、Ubuntu、Debian 等)上得到演示。
公开的漏洞利用 packet_edit_meme 可在 GitHub(sgkdev/packet_edit_meme)上获取,并针对 /bin/su。我们描述其基本逻辑,不包含破坏性载荷:```c
/* Pseudocode outline of the exploit (simplified) /
int main() {
/ 1. Identify a setuid binary (su) and its ELF entry offset */
int fd = open("/bin/su", O_RDONLY);
long entry = elf_entry_offset(fd);
if (entry < 0) abort();
printf("Target %s (UID=%d), entry offset 0x%lx\n", "/bin/su", getuid(), entry);
/* 2. Unshare user+net namespace to get CAP_NET_ADMIN locally */
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) abort();
/* Map UID/GID to root (handled via /proc/self/uid_map, /gid_map) */
// (omit details: write "0 <uid> 1" to /proc/self/uid_map and gid_map, and deny setgroups)
/* 3. Setup environment: bring up loopback and listener */
if (system("ip link set lo up") < 0) abort();
if (system("nc -l 127.0.0.1 9999 &") < 0) abort();