← LeisureLinux 文章索引
LeisureLinux · 微信公众号文章

Bad Epoll 深度拆解:一桩跨越三年、两条机器指令引发的内核血案

AI/Agent 阅读原文(微信)↗

核心观点:2023 年一次看似人畜无害的 commit,在 Linux 内核的 epoll 代码中埋下了两颗炸弹。一枚被 AI 发现并拆除,另一枚——Bad Epoll(CVE-2026-46242)——潜伏了整整三年,直到 2026 年 7 月才被公开披露。它让非特权用户就能拿到 root,而且能在 Chrome 沙箱里触发。

一、引言:一个不高不低的日子

2026 年 4 月 24 日,Linus Torvalds 合入了一个提交号为 a6dc643c6931 的补丁。提交信息只有一行:「eventpoll: fix ep_remove struct eventpoll」。不痛不痒,每周都能看到十几个类似的修复。

但这件事的背景远不止于此。

三个月后,这个补丁修复的漏洞被以「Bad Epoll」的名字披露,CVE 编号 CVE-2026-46242,CVSS 7.8(High)。它的发现者 Jaeyoung Chung——首尔大学计算机安全实验室的博士生——早在半年前就把它作为零日漏洞提交给了 Google 的 kernelCTF 项目。

为什么一个号称\"高危\"的漏洞,修复了这么久才公开?因为它触及了 Linux 内核最核心、最敏感的组件之一——epoll。而真正令人不寒而栗的是:这篇代码里藏着一个陷阱,即使是最先进的 AI 安全分析工具也只找到了其中一个。

二、前置知识:epoll 是什么,为什么人人都怕动它

如果你是后端工程师,大概率每天都在用 epoll——只是你不知道。

int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 然后可以 epoll_wait 等待事件

Nginx、Redis、Node.js、Netty......几乎所有高并发框架的底层都有一个 epoll 实例在工作。epoll 是 Linux 高性能 I/O 的基石。

它的内核实现(fs/eventpoll.c,约 2500 行)维护了两个关键结构:

  • struct eventpoll:一个 epoll 实例对应的数据结构,包含一个红黑树(用于管理注册的 fd)和一个就绪链表。
  • struct epitem:每个被监视的文件描述符对应一个 epitem,挂在 eventpoll 的红黑树上。

当一个 fd 上有事件发生时,内核通过回调机制把对应的 epitem 加到就绪链表上,然后唤醒在 epoll_wait 上等待的进程。

这套机制从 Linux 2.5.45(2002 年)引入,稳定运行了二十多年。没人敢乱动这段代码——它太核心、太敏感了。

但 2023 年,有人动了。

三、罪魁祸首:2023 年那段\"好心\"的 commit

2023 年 4 月 8 日,提交 58c9b016e128 合并进了主线。它试图优化 epoll 的清理路径——具体来说,是 ep_remove() 函数中 file->f_ep 指针的清理逻辑。

改动意图是好的:减少锁持有时间,提高并发性能。

问题在于,这段代码在 file->f_lock 下清除了 file->f_ep 指针后,仍然继续使用 file 对象去完成后续操作——做 hlist_del_rcu() 链表操作,然后才释放锁。

// 简化后的漏洞代码
static void ep_remove(struct eventpoll *ep, struct epitem *epi)
{
struct file *file = epi->ffd.file;

ep_unregister_pollwait(ep, epi);
if (unlikely(READ_ONCE(epi->dying)))
return;

spin_lock(&file->f_lock);
// 🔴 这里清除了 f_ep 指针!
ep_remove_file(ep, epi, file);
// 但 file 对象还在继续使用...
if (ep_remove_epi(ep, epi))
// ...
spin_unlock(&file->f_lock);
// ... 之后才返回
}

这段代码看起来没什么大问题——确实,90% 的情况下它工作得很好。99.999% 的情况下都工作得很好。

但那 0.001% 的情况,恰好是一条潜在的 root 权限。

四、漏洞解剖:六条指令宽的竞态窗口

Bad Epoll 是一个经典的 Use-After-Free(UAF)竞争条件漏洞。它的触发路径极其精巧:

正常流程(不会出事)

  1. 用户进程创建 epoll 实例,关联一个\"普通\"文件描述符
  2. 进程通过 epoll_ctl(EPOLL_CTL_DEL) 或关闭 epoll fd 触发 ep_remove()
  3. ep_remove() 执行清理:清除 file->f_ep、从红黑树删除 epitem、释放锁
  4. 一切正常

竞态触发(Bad Epoll 登场)

关键在于,ep_remove() 在清除 file->f_ep 后到完成所有操作之间,有一个极小的窗口(大约六条 x86 机器指令宽)。

如果另一个线程/进程恰好在这个窗口内对这个 fd 调用 close()

  1. close() 触发内核的 __fput() 函数
  2. __fput() 检查 file->f_ep——发现是 NULL,于是它跳过了 eventpoll_release_file() 这个关键清理函数
  3. __fput() 直接调用 f_op->release释放了 struct file 对象
  4. 与此同时,ep_remove() 还在使用同一个 file 对象做 hlist_del_rcu() 链表操作

这就是 UAF:ep_remove() 用它认为还活着的指针,操作着已经被释放的内存

更危险的是,struct file 使用的是 SLAB_TYPESAFE_BY_RCU 内存分配策略,被释放的内存槽可能被 alloc_empty_file() 重新分配。攻击者可以利用这一点,让 kmem_cache_free() 错误地释放另一个 slab cache 的对象——这就是内核内存损坏的起点。

为什么难以发现

这个竞争窗口有多窄?大约六条机器指令。

在现代化 4GHz CPU 上,这大约是 1.5 纳秒。如果用人来类比,相当于你眨一次眼的功夫,这个窗口已经开合了 1 亿次。

普通的内核内存错误检测工具(如 KASAN)在这么短的窗口内几乎不可能触发。这也是为什么即使先修复了同系列的一个 bug(CVE-2026-43074),Bad Epoll 仍然藏了大半年的原因。

五、利用:99% 成功率不是吹的

读到这儿你可能会想:窗口这么窄,实际利用是不是只能靠运气?

Jaeyoung Chung 的答案是:不是。

他的利用策略非常聪明——可以概括为三个词:放大、重试、不崩溃

放大窗口

攻击者使用四个相互关联的 epoll 文件描述符:

  • 两个用于制造竞争(\"触发对\")
  • 两个作为\"受害者\"(用于接收损坏的内存)

通过精心编排 epoll 之间的依赖关系,攻击者有效放大了竞争窗口。不再是最初的六条指令——利用代码可以把窗口拉宽好几个数量级。

重试而不崩溃

内核漏洞利用最大的挑战不是成功,而是失败不 crash。如果每次失败都导致内核崩溃(kernel panic),攻击者无法反复尝试。

Chung 的 exploit 设计得极其精巧:在竞争失败时,它优雅地回退而不损坏内核状态。这意味着攻击者可以在同一个系统上反复触发竞争,直到成功——每次失败只是安静地返回,不留痕迹。

最终效果

在测试系统上,Chung 的 exploit 达到了 约 99% 的成功率。更令人警惕的是:

  • 它不需要 root 权限
  • 它可以从 Chrome 渲染器沙箱内部触发
  • 它可以在 Android 设备上实现 root——这对绝大多数 Linux 提权漏洞来说都不常见

六、AI 发现了一个,但漏了另一个

这个漏洞还有一个耐人寻味的\"彩蛋\":

2026 年初,Anthropic 的 AI 模型 Mythos 被用来审计 Linux 内核的 epoll 代码。Mythos 发现了一个 race condition——跟踪为 CVE-2026-43074,内核社区也及时打了补丁。

但是 Mythos 没有发现 Bad Epoll。

原因推测有三:

  1. 窗口更窄:Bad Epoll 的竞争窗口只有六条指令,远小于 Mythos 发现的另一个 bug。AI 模型可能倾向于发现触发条件更\"宽松\"的漏洞。
  2. 不容易触发 KASAN:KASAN(内核地址消毒器)是内核检测内存错误的主要工具。Bad Epoll 在另一个 bug 被修复后,不再触发 KASAN 告警,因为它走的是不同的代码路径。
  3. 修复掩盖问题:CVE-2026-43074 的补丁改了同一段代码,反而可能让 Bad Epoll 更难被发现——分析工具会认为\"这段代码已经修过了\"。

这提醒我们:AI 辅助安全分析虽然强大,但仍然是\"找已知模式\"——真正的 0-day 常常藏在\"已知模式\"之外。

七、修复方案:一条微妙的 guard

修复补丁 a6dc643c6931 的改动不大,但非常关键:

// 修复前
static void ep_remove(struct eventpoll *ep, struct epitem *epi)
{
struct file *file = epi->ffd.file;  // 直接拿指针

spin_lock(&file->f_lock);
ep_remove_file(ep, epi, file);
// 🔴 窗口:f_ep 已清,但 file 还在用
if (ep_remove_epi(ep, epi))
// ...
spin_unlock(&file->f_lock);
}

// 修复后
static void ep_remove(struct eventpoll *ep, struct epitem *epi)
{
struct file *file __free(fput) = NULL;

// ✅ 关键:用 epi_fget() 增加 file 引用计数
file = epi_fget(epi);
if (!file)
return;  // 如果拿不到引用,说明 eventpoll_release_file 已接手

spin_lock(&file->f_lock);
ep_remove_file(ep, epi, file);
// ✅ 现在 file 的引用计数保证了 __fput() 不会提前触发
if (ep_remove_epi(ep, epi))
// ...
spin_unlock(&file->f_lock);
// __free(fput) 自动释放引用
}

修复的核心思想是引用计数保护:在操作开始时,通过 epi_fget() 获取 file 对象的一个额外引用。这样:

  • 如果 __fput() 并发执行,它看到引用计数 > 1,不会释放对象
  • ep_remove() 完成所有操作后,__free(fput) 自动释放这个引用
  • 如果 eventpoll_release_file() 已经抢先清除了 epi->dying,则 epi_fget() 返回 NULL,ep_remove() 直接退出

这是一条经典的\"持有引用,延迟释放\"的防护模式——在内核代码中很常见,但在这个位置缺失了三年。

此外,还有两个相关修复补丁也被一并合入:

  • ced39b6a8062
  • ef4ca02e9536

八、影响范围与修复建议受影响

  • Linux 主线内核 ≥ 6.4(6.4 及以上所有版本,如果未回溯补丁)
  • Android 设备运行 6.6 系列或更新内核(包括当前的 Pixel 设备)
  • Linux 发行版:Ubuntu 24.04+、Fedora 38+、Debian 13+ 等使用较新内核的发行版

不受影响

  • Linux 6.1 及更早的内核(基于 6.1 的 Pixel 8 不受影响)
  • 所有已应用补丁的系统

临时缓解

由于 epoll 是内核核心组件,无法通过模块卸载或参数禁用来缓解。唯一的办法就是打补丁。

各发行版补丁追踪:

发行版 补丁状态 内核包
Ubuntu 24.04 已回溯 6.8.x linux-generic
RHEL 9 RHSA 评估中 kernel-5.14
Debian unstable 已合入 6.10-rc+ linux-image
Android GKI 已修复 AOSP 通用内核 android14-6.1+

九、几点思考① 性能优化的代价

Bad Epoll 的根源是一次\"优化\"——减少锁持有时间、提高并发性能。内核的安全漏洞中,相当一部分来源于优化引入的边界条件问题。每一次优化,都是在为复杂性买单。

② AI 安全分析还很年轻

Mythos 发现了一个 bug 而漏了另一个,这件事本身并不可耻——人类专家也会漏。但它清楚地划出了一条线:当前的 AI 安全分析擅长\"找相似\",不擅长\"发现意外\"。

③ epoll 该重写了吗?

一个 2500 行的文件,二十多年的历史,两个平行的 race condition 在同一段代码里藏了三年。epoll 的代码复杂度已经积重难返。有人(包括我自己)开始讨论:epoll 是否应该被 io_uring 替代? 至少在新的应用开发中,长远的趋势已经很明显。

④ Zero-day 的生命周期

从 2023 年 4 月引入到 2026 年 4 月修复,Bad Epoll 潜伏了整整三年。这三年里,无数系统运行着有漏洞的内核——包括你的手机,在你的口袋里。

参考文献

  1. CVE-2026-46242 - NVD
  2. Linux kernel commit a6dc643c6931 - eventpoll: fix ep_remove struct eventpoll
  3. Linux kernel commit 58c9b016e128 - epoll optimization (original bug)
  4. The Hacker News - Bad Epoll Critical Linux Kernel Flaw
  5. Cyber Security News - Bad Epoll Linux Kernel Vulnerability
  6. SentinelOne Blog - Bad Epoll CVE-2026-46242 Analysis
  7. TuxCare - CVE-2026-46242 Bad Epoll Vulnerability
  8. Red Hat Security - CVE-2026-46242
  9. Tenable - CVE-2026-46242
  10. Linux man page - epoll(7)

后记: 这篇文章的技术分析参考了多名安全研究者的公开分析报告。Bad Epoll 的完整 exploit 代码已在 Google kernelCTF 项目中提交,预计 90 天后公开。如果你运行着 6.4+ 内核的 Linux 桌面或服务器,请检查内核版本并及时更新。

━━━ ━━━ ━━━

*本文完。欢迎关注 LeisureLinux,获取更多深度技术解读。*

CVE-2026-3071 \"Dirty Frag\":Linux 内核权限提升漏洞深度解析

Copy Fail 内核漏洞已经在各大主流发行版 Ubuntu, Debian 修复,请尽快升级!

Linux 内核潜伏型漏洞全解析:从 Netfilter 到 CVE-2026-31431 (Copy Fail)

隐匿的威胁:论 Linux 内核漏洞的"长寿"现象与企业级补丁防御策略

六年、360 个补丁:Linux 内核终于把 strncpy() 扫地出门

解构 NGINX 并发神话:从 epoll 内核原理到 ET/LT 模式的深度博弈

彻底洞察 Linux 进程行为:strace 高阶实战手册

常见问题(FAQ)

Q1:这篇文章主要讲什么? 2026 年 4 月 24 日,Linus Torvalds 合入了一个提交号为 a6dc643c6931 的补丁。提交信息只有一行:「eventpoll: fix ep_remove struct eventpoll」。 Q2:还有哪些关键事实? 此外,还有两个相关修复补丁也被一并合入: - ced39b6a8062 - ef4ca02e9536 八、影响范围与修复建议受影响 - Linux 主线内核 ≥ 6.4(6.4 及以上所有版本,如果未回溯补丁) - Android 设备运行 6.6 系列或更新内核(包括当前的 Pixel 设备) - Linux 发行版:Ubu… Q3:有哪些值得注意的细节? 它的内核实现(fs/eventpoll.c,约 2500 行)维护了两个关键结构: - struct eventpoll:一个 epoll 实例对应的数据结构,包含一个红黑树(用于管理注册的 fd)和一个就绪链表。 Q4:核心结论是什么? 它的触发路径极其精巧: 正常流程(不会出事) 1. 用户进程创建 epoll 实例,关联一个\"普通\"文件描述符 2. 进程通过 epoll_ctl(EPOLL_CTL_DEL) 或关闭 epoll fd 触发 ep_remove() 3. ep_remove() 执行清理:清除 file->f_ep、从红黑树删除 epitem、释放锁 4…

本文整理自微信公众号 LeisureLinux 的原创内容(Linux / AI / 安全 硬核技术)。

· 阅读原文(微信公众号)

· 关注公众号 LeisureLinux,第一时间获取技术深度内容。

LeisureLinux 公众号二维码(微信扫一扫关注)