一、漏洞速览

CVE-2026-31694(QVD-2026-23425)是 Linux 内核 FUSE(Filesystem in Userspace)子系统中的一个高危本地提权漏洞。该漏洞源于 fs/fuse/readdir.cfuse_add_dirent_to_cache() 函数缺失对单条 dirent 记录长度是否超过 PAGE_SIZE 的边界检查,导致一条超长目录项记录可越界写入物理相邻的下一个内核页。由于 readdir 缓存页来自页分配器而非 slab 分配器,溢出落在内核 direct map 上物理相邻的页面,KASAN 不会在页之间插桩红区,因而该溢出在默认 KASAN 内核中长期静默。

属性
CVE 编号 CVE-2026-31694
厂商编号 QVD-2026-23425
CVSS 3.1 7.8 HIGH(AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
漏洞类型 内核 page cache 越界写(OOB Write)
受影响组件 Linux 内核 FUSE 子系统 fs/fuse/readdir.c
漏洞函数 fuse_add_dirent_to_cache()
根因 commit 69e34551152a(2018,引入潜伏缺陷)
武装 commit 27992ef80770d(2024-12,v6.15,使缺陷可达)
修复 commit 51a8de6c50bf9(2026-04-20)
报告者 Qi Tang、Zijun Hu(0xCyberstan)
PoC / EXP 已公开

该漏洞允许任意已能在节点上执行用户态代码的非特权用户,借助 FUSE 挂载能力构造超长目录项名,触发 page cache 越界写,进而篡改特权进程读取的文件缓存页(如 /etc/passwd)或破坏 SUID 二进制文件的 .init 段,最终获得 root 权限。利用思路与 DirtyPipe(CVE-2022-0847)一脉相承,但触发原语完全不同。


二、FUSE 内核机制

2.1 架构总览

FUSE(Filesystem in Userspace)允许非特权用户在用户态实现完整的文件系统语义。其由三部分组成:

  • 内核模块 fuse.ko:将 FUSE 文件系统的 VFS 调用转化为请求,通过 /dev/fuse 字符设备与用户态通信。
  • 用户态库 libfuse:提供 daemon 框架,从 /dev/fuse 读取请求、分发回调、写回响应。
  • 挂载工具 fusermount:协助非特权用户完成挂载操作(通常以 setuid root 安装)。

核心设计思想是"内核作为客户端、用户态 daemon 作为服务端"——内核不实现文件系统语义,只负责转发;语义正确性完全由 daemon 决定。这意味着一个恶意 daemon 可以返回任意畸形数据,内核必须对返回内容做充分校验,否则即构成攻击面。

用户态进程            内核态                用户态 daemon
[ls /fuse]──VFS──▶[fuse.ko (客户端)]    [libfuse daemon (服务端)]
                       │ readdir()            │ readdir_cb()
                       ▼ 构造请求              ▲ 构造响应
                   [/dev/fuse] ◄──双向通道──► [read/write]
                       │ 解析响应              │ dirent[]
                       ▼                       namelen=可控(攻击面)
                   [page cache (可选缓存)]

  关键安全特性: 允许非特权用户挂载自定义文件系统
  攻击面: daemon 返回的 dirent 数据由攻击者完全控制

2.2 dirent 协议结构

FUSE 的 readdir 请求返回一组 fuse_dirent 记录。每条记录是变长的,其结构定义如下:

struct fuse_dirent {
    uint64_t ino;      // inode 号
    uint64_t off;      // 下一项偏移
    uint32_t namelen;  // 文件名长度
    uint32_t type;     // 文件类型
    char name[];       // 变长文件名
};
#define FUSE_NAME_OFFSET offsetof(struct fuse_dirent, name)  // = 24
#define FUSE_DIRENT_SIZE(d) FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + (d)->namelen)

一条 dirent 的总长度 reclennamelen 决定,并对齐到 FUSE_DIRENT_ALIGN(8 字节)。其中 FUSE_NAME_OFFSET 固定为 24 字节。这意味着:

  • namelen 较小时,reclen 远小于 PAGE_SIZE(4096)。
  • namelen 达到上限时,reclen = FUSE_REC_ALIGN(24 + namelen) 可能超过 PAGE_SIZE

FUSE_NAME_MAX 的定义在历史上发生过关键变更:2018 年为 1024,2024 年 12 月被提升至 PATH_MAX - 1 = 4095。正是这次提升,将一个潜伏六年的边界缺陷变为可利用的武装漏洞。


三、FUSE 与 page cache 交互

3.1 FOPEN_CACHE_DIR 缓存机制

FUSE daemon 在打开目录时可以设置 FOPEN_CACHE_DIR 标志。设置后,内核会将 readdir 返回的 dirent 记录复制到 page cache 中,后续对该目录的 getdents 调用可直接从缓存页读取,无需再次往返用户态 daemon。这是一项重要的性能优化。

3.2 page cache 的三个关键属性

page cache 本质上具有三个对利用至关重要的属性:

  1. 共享性:整个系统只有一份文件缓存副本。所有进程读取同一文件时,访问的是同一组物理页。
  2. 被信任性:内核假定 page cache 内容与磁盘一致、未被篡改。特权进程(susudo、PAM 认证模块等)都从同一份缓存页读取数据,不会重新校验。
  3. 页粒度:缓存以页为单位管理(x86_64 上 PAGE_SIZE = 4096 字节)。不同文件的缓存页在物理内存中可能相邻。
特权进程                      非特权进程                内核 page cache
[su root / PAM]            [exploit (FUSE daemon)]   [全局物理内存, 共享一份]
     │ read(/etc/passwd)         │ readdir → 写入缓存(越界!)    │
     │                           │                              │
     ▼                           ▼                              ▼
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
  │ FUSE readdir│ │ /etc/passwd │ │ FUSE readdir│ │ 溢出的24 │
  │ 缓存页 (P0) │ │ 缓存页 (P1) │ │ 缓存页 (P2) │ │ 字节越界 │
  └─────────────┘ └──────▲──────┘ └─────────────┘ └────│─────┘
                         │ 特权进程读这里 ◄── 被篡改 ◄── OOB写入

  关键: su/PAM 认证从同一份缓存页读取, 内核不重新校验
  溢出破坏全局内核 page cache, 不受 namespace 隔离影响

第三点尤为重要:FUSE readdir 缓存页来自页分配器(alloc_page),而非 slab 分配器。这意味着缓存页在内核 direct map 中物理相邻的下一页,可能是任意其他文件的 page cache 页、内核数据结构页,甚至 SUID 二进制文件的代码页。这为跨文件篡改提供了物理基础。


四、漏洞原理

4.1 缺陷本质:缺失的整页边界检查

fuse_add_dirent_to_cache() 在将一条 dirent 记录写入缓存页时,只检查"当前页剩余空间"是否足够放下记录,但从不检查"记录本身"是否超过 PAGE_SIZE。函数隐含一个未经校验的假设:任何单条 dirent 记录都能放入一个完整页

namelenFUSE_NAME_MAX 限制为 1024 时,最大 reclen 仅约 1048 字节,这个假设成立,缺陷不可达。但当 FUSE_NAME_MAX 在 v6.15 被提升至 4095 后,最大 reclen 达到 4120 字节,已超过 PAGE_SIZE,假设被打破,缺陷变为可达。

4.2 越界写计算

当恶意 FUSE 服务端返回 namelen = 4095 的目录项时:

reclen    = FUSE_REC_ALIGN(24 + 4095)
          = FUSE_REC_ALIGN(4119)
          = 4120 字节            (对齐到 8 字节边界)

overflow  = 4120 - 4096
          = 24 字节              (溢出到下一页)

memcpy(addr + offset, dirent, reclen) 执行时,当 offset = 0(即记录从页起始写入)时,reclen = 4120 字节中有 4096 字节写入当前页,尾部 24 字节溢出到物理相邻的下一个内核页。这 24 字节的内容完全由 daemon(攻击者)控制——它正好是 dirent 头部结构(inooffnamelentype),可被精心构造。

┌──────────────────────────────────────────────────────────────────────┐
│                      越界写原理图 (offset=0 场景)                    │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   当前 readdir 缓存页 (4096 字节)          物理相邻的下一页          │
│  ┌────────────────────────────────────┐   ┌────────────────────┐    │
│  │ offset=0                           │   │ 被溢出覆盖的24字节  │    │
│  │ ▼                                  │   │ ▼                  │    │
│  │ ┌────────────────────────────────┐ │   │ ┌────────────────┐ │    │
│  │ │ dirent 头部 (24B)              │ │   │ │ ino  (8字节)   │ │    │
│  │ │   ino / off / namelen / type   │ │   │ │ off  (8字节)   │ │    │
│  │ ├────────────────────────────────┤ │   │ │ nlen (4字节)   │ │    │
│  │ │                                │ │   │ │ type (4字节)   │ │    │
│  │ │   name[4072 字节]              │ │──▶│ └────────────────┘ │    │
│  │ │   (填充到页尾)                  │ │溢出│ ↑ 攻击者完全可控   │    │
│  │ │                                │ │   │                   │    │
│  │ └────────────────────────────────┘ │   │ 该页可能是:        │    │
│  │           (4096 字节写满)           │   │ - /etc/passwd 缓存 │    │
│  └────────────────────────────────────┘   │ - SUID 二进制代码页 │    │
│              ▲                            │ - 内核数据结构页    │    │
│              │                            └────────────────────┘    │
│              │                                       ▲              │
│              │ memcpy(addr, dirent, 4120)            │              │
│              │ 实际写入 4120 字节                     │              │
│              └───────────────────────────────────────┘              │
│                                                                      │
│  检查逻辑 [2]: if (offset + reclen > PAGE_SIZE)                      │
│    offset=0, reclen=4120 → 0 + 4120 > 4096 → true                   │
│    → index++; offset=0  (跳到新页起始)                               │
│    → 但从未检查 reclen > PAGE_SIZE 本身!                             │
│    → 新页 offset=0 时, 4120 字节 memcpy 仍然越界                     │
└──────────────────────────────────────────────────────────────────────┘

4.3 漏洞演化史:六年潜伏

下表完整记录了该缺陷从引入、潜伏、武装到修复的全过程:

时间 commit 事件 FUSE_NAME_MAX 最大 reclen 状态
2018 v4.20 69e34551152a 引入 readdir 缓存机制 1024 ~1048B 潜伏不可达
2024-12 v6.15 27992ef80770d 提升至 PATH_MAX-1 4095 4120B 武装可达
2026-04 51a8de6c50bf9 添加 reclen > PAGE_SIZE 检查 4095 - 修复

可见,FUSE_NAME_MAX 从 1024 提升至 4095 是漏洞武装化的关键拐点:在此之前最大 reclen(约 1048B)远小于 PAGE_SIZE,缺陷不可达;提升后最大 reclen(4120B)超过 PAGE_SIZE,缺陷变为可利用的越界写原语。

4.4 为何 KASAN 静默

KASAN(Kernel Address Sanitizer)是内核最常用的内存错误检测工具,但它对本次溢出完全静默。原因在于溢出目标不在 slab 上:

  • readdir 缓存页通过 find_or_create_page()alloc_page()页分配器(buddy allocator)分配,而非 slab 分配器。
  • KASAN 只在 slab 对象之间插桩红区(redzone),用于检测 slab 内的越界访问。
  • 页分配器分配的页之间没有 KASAN 红区。
  • 溢出的 24 字节落在内核 direct map 中物理相邻的下一个页,该页可能是合法分配的另一个页,KASAN 不认为这是越界。
Slab 分配器 (kmalloc) —— KASAN 有红区, 越界立即报告 ✓
  [redzone][obj A][redzone][obj B][redzone]
                ▲ 越界触发报告

页分配器 (alloc_page) —— KASAN 无红区, 溢出静默 ✗
  [Page N: readdir缓存页 4096B][Page N+1: 相邻页(任意用途)]
           ◄── 4120B memcpy ──▶  ▲ 溢出24字节, 无红区
  溢出落在 direct map 物理相邻页, KASAN 不认为是越界

这使得该漏洞在带 KASAN 的调试内核中也能长期隐藏,直到攻击者主动构造 PoC 才被发现,大大增加了其在野利用的隐蔽窗口。


五、漏洞代码

以下是修复前 fs/fuse/readdir.cfuse_add_dirent_to_cache() 的完整代码,关键位置以注释标注:

// fs/fuse/readdir.c (修复前)
static void fuse_add_dirent_to_cache(struct file *file,
                   struct fuse_dirent *dirent, loff_t pos)
{
    struct fuse_inode *fi = get_fuse_inode(file_inode(file));
    size_t reclen = FUSE_DIRENT_SIZE(dirent);   // [1] reclen 由服务端 namelen 决定, 完全可控
    pgoff_t index;
    struct page *page;
    loff_t size;
    u64 version;
    unsigned int offset;
    void *addr;

    spin_lock(&fi->rdc.lock);
    if (fi->rdc.cached || pos != fi->rdc.pos) {
        spin_unlock(&fi->rdc.lock);
        return;
    }
    version = fi->rdc.version;
    size = fi->rdc.size;
    offset = size & ~PAGE_MASK;        // 当前页内偏移
    index = size >> PAGE_SHIFT;        // 当前页索引
    if (offset + reclen > PAGE_SIZE) { // [2] 只检查"当前页剩余空间"是否够放
        index++;                       //     不够则换页
        offset = 0;                    // [3] 重置到新页起始 (offset=0)
    }
    spin_unlock(&fi->rdc.lock);

    if (offset) {
        page = find_lock_page(file->f_mapping, index);
    } else {
        page = find_or_create_page(file->f_mapping, index,
                     mapping_gfp_mask(file->f_mapping));
    }
    if (!page)
        return;

    spin_lock(&fi->rdc.lock);
    if (fi->rdc.version != version || fi->rdc.size != size ||
        WARN_ON(fi->rdc.pos != pos))
        goto unlock;

    addr = kmap_local_page(page);
    if (!offset) {
        clear_page(addr);              // 新页清零
        SetPageUptodate(page);
    }
    memcpy(addr + offset, dirent, reclen);  // [4] 越界写发生处!!!
                                            //     offset=0 时写入 4120 字节
                                            //     尾部 24 字节溢出到相邻页
    kunmap_local(addr);
    ...
}

漏洞链路分析:

  • [1] reclen = FUSE_DIRENT_SIZE(dirent)reclen 由 daemon 返回的 dirent->namelen 决定,攻击者完全可控。当 namelen = 4095 时,reclen = 4120
  • [2] if (offset + reclen > PAGE_SIZE):这是唯一的边界检查,但它只判断"当前页剩余空间是否够放记录"。当 offset = 0reclen = 4120 时,0 + 4120 > 4096 为真,代码进入换页分支。但换页后 offset 被重置为 0,问题并未解决——新页同样放不下 4120 字节。
  • [3] offset = 0:换页后从新页起始位置写入。
  • [4] memcpy(addr + offset, dirent, reclen):当 offset = 0reclen = 4120 时,memcpy 向页起始地址写入 4120 字节,其中 4096 字节填充当前页,尾部 24 字节溢出到物理相邻的下一个内核页。

整个逻辑从未检查 reclen > PAGE_SIZE 这一根本条件。这就是"缺失的整页边界检查"。


六、补丁 diff

修复补丁(commit 51a8de6c50bf9)极其简洁,仅新增 4 行代码,在进入锁之前即拦截超长记录:

diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c88194e52d18..db5ae8ec1030 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -41,6 +41,10 @@ static void fuse_add_dirent_to_cache(struct file *file,
 	unsigned int offset;
 	void *addr;
 
+	/* Dirent doesn't fit in readdir cache page?  Skip caching. */
+	if (reclen > PAGE_SIZE)
+		return;
+
 	spin_lock(&fi->rdc.lock);

补丁在函数最早期、获取自旋锁之前,即判断 reclen > PAGE_SIZE。若单条记录超过一页大小,直接跳过缓存(return),既不分配页也不写入。这是一种"安全降级"策略——放弃对该超长目录项的缓存优化,但彻底消除越界写风险。

该补丁的放置位置也经过考量:在锁之前返回,避免了不必要的锁争用和页分配开销,同时保证在任何并发场景下都不会执行到 memcpy


七、非特权挂载路径

漏洞利用的前提是攻击者能够挂载一个自定义 FUSE 文件系统。在主流 Linux 发行版上,非特权用户有两条默认可用的挂载路径:

路径 A:fusermount3(setuid root)

fusermount3 工具通常以 setuid root 权限安装,是 Ubuntu、Fedora、Mint 等发行版的默认配置。它允许非特权用户完成 FUSE 挂载的最终步骤(mount(2) 系统调用需要特权),而 daemon 本身以普通用户身份运行。

路径 B:非特权 user namespace

通过 unshare -Ufirmp 创建新的 user namespace 和 mount namespace,非特权用户可在新命名空间内获得"伪 root"权限,从而直接调用 mount(2) 挂载 FUSE。Debian 11+、Arch、RHEL 9、Fedora 等发行版默认允许非特权 user namespace。

路径 A: fusermount3 (setuid root)     路径 B: 非特权 user namespace
[普通用户 uid=1000]                    [普通用户 uid=1000]
    │ exec                                 │ unshare -Ufirmp
    ▼                                      ▼
[fusermount3 (euid=0)]                 [新 user ns (ns内uid=0)]
    │ mount(2)                             │ mount(2)
    ▼                                      ▼
  FUSE 挂载成功                           FUSE 挂载成功
  Ubuntu/Fedora/Mint 默认安装             Debian 11+/Arch/RHEL 9/Fedora 默认允许

  ⚠ 关键: 无论走哪条路径, 溢出破坏的都是全局内核 page cache
     page cache 是全局资源, 不受 namespace 隔离影响
     新 namespace 内的越界写可影响宿主机和其他 namespace 的缓存页

关键点在于:虽然挂载发生在用户命名空间内,但 page cache 是全局内核资源,不受 namespace 隔离。新 namespace 内触发的越界写,可以影响宿主机和所有其他 namespace 的缓存页——包括特权进程读取的 /etc/passwd 和 SUID 二进制文件。这使得 user namespace 隔离在这一攻击模型下形同虚设。


八、提权利用思路

思路 1:篡改 /etc/passwd 页缓存(0xCyberstan PoC)

这是公开 PoC 采用的思路,与 DirtyPipe(CVE-2022-0847)的利用范式高度相似。攻击者利用 24 字节溢出改写 page cache 中 /etc/passwd 的 root 行,将密码字段置空:

原始: root:x:0:0:root:/root:/bin/bash
篡改: root::0:0:root:/root:/bin/sh
           ▲
           └── 密码字段置空 (x → 空)

配合 PAM 的 nullok 选项(Ubuntu 默认配置允许空密码认证),攻击者执行 su root 时输入空密码即可成功提权。磁盘上的 /etc/passwd 并未被修改,仅内存中的 page cache 副本被篡改,重启后恢复原状。

┌──────────────────────────────────────────────────────────────────────┐
│              思路1: /etc/passwd 页缓存篡改利用链                      │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Step 1: 堆风水 groom —— 让 FUSE readdir 缓存页物理相邻于            │
│          /etc/passwd 的 page cache 页                                │
│                                                                      │
│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐             │
│  │ FUSE 页 P0 │  │ passwd 页  │  │  (groom 到相邻)     │             │
│  │ readdir    │  │ /etc/passwd│  │                    │             │
│  │ 缓存       │  │ root:x:... │  │                    │             │
│  └────────────┘  └────────────┘  └────────────────────┘             │
│                                                                      │
│  Step 2: 恶意 FUSE daemon 返回 namelen=4095 的目录项                 │
│          构造溢出的 24 字节 = 篡改后的 passwd root 行片段             │
│                                                                      │
│  Step 3: 触发 readdir → fuse_add_dirent_to_cache()                   │
│          memcpy 写入 4120 字节, 尾部 24 字节溢出到 passwd 页          │
│                                                                      │
│  ┌────────────────────┐  ┌────────────────────────────┐             │
│  │ FUSE readdir 页     │  │ /etc/passwd 页 (被篡改)     │             │
│  │ [4120B dirent]      │─▶│ root::0:0:root:/root:/bin/sh│             │
│  └────────────────────┘  └────────────────────────────┘             │
│                                    ▲ 24字节覆盖                      │
│                                                                      │
│  Step 4: su root (空密码) ──▶ PAM nullok 认证通过 ──▶ root shell     │
└──────────────────────────────────────────────────────────────────────┘

思路 2:SUID 二进制 .init 段破坏

另一条利用路径是针对 SUID 二进制文件(如 /usr/bin/su)的 .init 段。24 字节溢出恰好足够放置一段精简的 shellcode,在程序入口处执行 setuid(0) + setgid(0) 后正常返回,使 SUID 程序以 root 身份运行后续逻辑。

24 字节 shellcode 如下(x86_64):

static const uint8_t payload[24] = {
    0xf3,0x0f,0x1e,0xfa,  /* endbr64                       */
    0x31,0xff,              /* xor edi, edi ; rdi=0          */
    0x6a,0x69, 0x58,        /* push 0x69; pop rax ; rax=105  */
    0x0f,0x05,              /* syscall ; setuid(0)           */
    0x6a,0x6a, 0x58,        /* push 0x6a; pop rax ; rax=106  */
    0x0f,0x05,              /* syscall ; setgid(0)           */
    0xc3,                   /* ret                           */
    0x90,0x90,0x90,0x90,0x90,
};

该 shellcode 依次调用 setuid(0)(syscall 105)和 setgid(0)(syscall 106),将进程凭证提升为 root,然后 ret 返回正常执行流。由于 SUID 程序本身已以 root 的 effective uid 运行,setuid(0) 可将 real uid 也设为 0,完成完整提权。

/usr/bin/su 的 page cache (代码段)
  .init 段入口: [endbr64 (4B)] ←── 被 24 字节 shellcode 覆盖
                    ▲
                    │ 24 字节溢出
  [FUSE readdir 页 (dirent 4120B)] → memcpy 尾部 24B 落在 .init 段入口

  执行流: su 启动 → .init 段 → shellcode 执行
          → setuid(0) + setgid(0) → ret → 正常逻辑 → root
  磁盘上 /usr/bin/su 不变, 仅 page cache 副本被改 (无痕)

堆风水(页相邻 groom)

无论哪种思路,利用的核心难点都在于"堆风水"——如何让 FUSE readdir 缓存页物理相邻于目标文件的 page cache 页。这需要精确控制页分配器的分配顺序:

  • 排空 per-CPU page 空闲链表:通过大量分配消耗当前 CPU 的 PCP(per-CPU page)缓存,迫使后续分配从 buddy allocator 取连续页。
  • 牺牲文件池:用 pread() 反复将目标文件 fault 入缓存,再用 posix_fadvise(FADV_DONTNEED) 驱逐,反复"占位—释放"以调整物理页布局。
  • 概率性:页相邻 groom 是概率性的,未命中可能导致溢出落在无关页上引发内核 panic。因此公开 PoC 建议仅在虚拟机中运行,并通过多次重试提高命中率。
排空 PCP 空闲链表 → pread fault 目标文件入缓存占位
  → find_or_create_page 分配 readdir 页 (物理相邻目标页)
  → memcpy 越界写命中 → 概率性: 未命中可能 panic, 仅限 VM

九、受影响版本

由于漏洞的可达性取决于 FUSE_NAME_MAX 是否被提升至 4095,实际可利用的版本范围为 v6.15 及以后。但 NVD 记录的影响范围覆盖了从引入 readdir 缓存机制的 v4.20 起的所有版本(因为代码缺陷本身自 2018 年起即存在)。

维度 范围
实际可利用版本 v6.15+(FUSE_NAME_MAX 提升后)
NVD 影响范围 4.20–6.6.136、6.7–6.12.84、6.13–6.18.25、6.19–7.0.2
修复版本 6.18.25+、7.0.2+、7.1+
已知受影响发行版 Ubuntu 26.04 LTS(内核 7.0.0-14-generic

值得注意的是,虽然 NVD 将 v4.20 起的所有版本都列为受影响,但在 v6.15 之前,由于 FUSE_NAME_MAX = 1024 的限制,最大 reclen 不会超过 PAGE_SIZE,漏洞不可达。真正需要紧急关注的是 v6.15 至修复版本之间的内核,尤其是使用这些版本的 LTS 发行版。


十、复现环境

公开 PoC 仓库为 0xCyberstan/CVE-2026-31694-POC,提供两种运行模式:

# Marker 模式: 测试堆风水命中率 (不实际提权)
./exploit --poc -n 20

# LPE 模式: 完整提权利用
# 5 轮 warmup 后最多 200 次尝试
./exploit
  • --poc marker 模式:不执行实际提权,仅通过 marker 字节验证溢出是否命中目标页,用于评估堆风水命中率。建议先用此模式在目标环境中调参。
  • LPE 模式:执行完整提权链。由于堆风水的概率性,程序内置 5 轮 warmup 预热后最多重试 200 次。未命中可能导致内核 panic,强烈建议仅在虚拟机中运行。

十一、防御方案

11.1 根本性防御

方案 措施 效果
升级内核 更新至 6.18.25+、7.0.2+ 或 7.1+ 彻底修复,消除越界写
限制 /dev/fuse 收紧字符设备访问权限 阻止非特权用户使用 FUSE
移除 setuid 位 chmod u-s /usr/bin/fusermount3 封堵路径 A 挂载
禁用非特权 user namespace kernel.unprivileged_userns_clone=0 封堵路径 B 挂载
监控异常 FUSE 挂载 审计 /proc/mounts/dev/fuse 访问 检测利用尝试

11.2 纵深防御建议

层级1 内核补丁    → 升级到 6.18.25+ / 7.0.2+ / 7.1+
层级2 FUSE控制    → 移除 fusermount3 setuid 位 / 限制 /dev/fuse 权限 / 禁用非特权 user namespace
层级3 监控检测    → 审计 /dev/fuse 打开 / 监控 fuse 挂载 / 检测超长目录项名 / 关注空密码认证
层级4 认证加固    → 禁用 PAM nullok (禁止空密码认证)

对于无法立即升级内核的环境,层级 2 和层级 4 的缓解措施尤为重要:移除 fusermount3 setuid 位和禁用非特权 user namespace 可直接切断攻击者的挂载能力;禁用 PAM nullok 则可瓦解思路 1 的提权路径(即使 page cache 被篡改,空密码认证也会被拒绝)。


总结

CVE-2026-31694 是一个典型的"潜伏缺陷 + 环境变更 = 武装漏洞"案例。其根因——fuse_add_dirent_to_cache() 中缺失的 reclen > PAGE_SIZE 检查——自 2018 年起即存在于内核中,但因 FUSE_NAME_MAX = 1024 的限制而不可达,潜伏长达六年。2024 年 12 月 FUSE_NAME_MAX 被提升至 4095 后,最大 reclen 达到 4120 字节,超过 PAGE_SIZE,缺陷瞬间变为可利用的越界写原语。该漏洞触发门槛低(非特权用户即可挂载 FUSE)、检测难度高(KASAN 对页分配器页间溢出静默)、影响范围广(page cache 为全局资源,namespace 隔离无效)、利用思路成熟(与 DirtyPipe 同源,公开 PoC 可参考)。

修复方案仅需 4 行代码即可彻底消除风险。这一案例再次印证了内核安全中"隐含假设"的危险性:当一个函数的边界检查依赖于某个外部常量(FUSE_NAME_MAX)的不变量时,该常量的任何变更都应触发对依赖代码的重新审计。FUSE_NAME_MAX 从 1024 提升到 4095 时,若能同步审查所有以 PAGE_SIZE 为隐含上界的 FUSE 代码路径,本可避免这一漏洞的武装化。


免责声明
本文仅供网络安全教育、防御研究和授权安全测试使用。所有漏洞分析基于公开 CVE 公告、上游内核 commit 及公开 PoC 仓库。文中技术细节旨在帮助安全工程师与内核开发者理解漏洞原理并构建防御体系。读者必须在完全自有的隔离测试环境(如虚拟机)中使用相关技术,未经授权对任何真实系统进行渗透测试或提权操作属于违法行为。作者不对任何因不当使用本文信息而造成的后果承担责任。