FUSE readdir缓存中的越界写入导致非特权用户提权至root (CVE-2026-31694)
摘要
Linux内核FUSE readdir缓存中的一个漏洞(CVE-2026-31694)允许无特权的本地用户通过越界写入的方式提权至root。该漏洞影响许多桌面发行版,可通过挂载FUSE文件系统进行利用。
<p><a href="https://lobste.rs/s/0kc445/unprivileged_root_via_out_bounds_write">评论</a></p>
查看缓存全文
缓存时间:
2026/07/02 02:06
# 利用 FUSE readdir 缓存中的越界写入实现非特权提权 (CVE-2026-31694) – cyberstan
来源:https://cyberstan.co.uk/fuse-readdir-oob/
## 通过 FUSE readdir 缓存中的越界写入实现非特权提权 (CVE-2026-31694)
CVE-2026-31694 · 已在主线中修复 (51a8de6 (https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=51a8de6c50bf947c8f534cd73da4c8f0a13e7bed)),2026年4月20日,Cc: stable · 修复了 69e3455 · 自 69e3455 (2018) 起潜伏 · 受影响版本:v6.16 至修复版本 · 由 Qi Tang 和 Zijun Hu 报告 · 由 Samuel Page 提交修复
## 摘要
`fs/fuse/readdir.c` 中缺少一个边界检查,允许非特权本地用户在可控的 24 字节写入内核页末尾之后,并且通过将该写入命中到 `/etc/passwd` 的缓存副本上,提权至 root。`fuse_add_dirent_to_cache()` 将目录项(其长度取自服务器控制的 `namelen` 字段)复制到单个页面缓存页中。代码会检查该目录项是否适合当前页的剩余空间,如果不适合,则前进到一个新页面,但它从未检查该目录项是否*完全*适合一个页面。返回 `namelen=4095` 的 dirent 的 FUSE 守护进程会产生一个 4120 字节的记录,超出 `PAGE_SIZE` 的 24 字节会溢出到物理上紧随其后的任何内核页中。FUSE 守护进程以挂载文件系统的非特权用户身份运行,并且在普通桌面环境中挂载 FUSE 文件系统无需真正的特权,因此整个过程可以从一个普通会话中触发,无需任何能力、无需 setuid 辅助程序(除了每个发行版都自带的那个)、也无需容器逃逸。
该溢出落在页分配器的直接映射中,而不是 slab 对象中,这意味着 KASAN 保持沉默,而有趣的邻居不是堆结构,而是相邻的页,包括内核已信任的只读文件的页缓存。
该漏洞编号为 CVE-2026-31694。由 Qi Tang 和 Zijun Hu 报告,由 Samuel Page 修复(根据提交标签,有 Bynario AI 的协助),补丁于 2026 年 4 月 20 日合并。以下是我自己对漏洞的根因分析和利用过程。其中关于可触达性的考古部分尤其值得讲述。
## 非特权本地攻击面
溢出发生在内核处理来自 FUSE 守护进程的 `READDIR` 回复时。守护进程只是一个用户空间程序,以启动它的用户身份运行,因此问题仅在于非特权用户能否挂载 FUSE 文件系统并将回复送入 readdir 缓存。在默认安装中,存在两条独立的路径,它们共同覆盖了几乎所有主要发行版。
### 路径 A:fusermount3(setuid root,无需命名空间)
`fusermount3` 是一个小的 setuid-root 辅助程序,它代表用户执行特权的 `mount(2)` 调用。它在任何使用 GNOME 的系统中默认提供,因为 `gvfs-fuse` 依赖于 `fuse3`,而 `gvfs-fuse` 被 `ubuntu-desktop` 元包及其等价物带入。具体来说,任何非特权用户无需任何设置即可立即触发该漏洞的发行版包括:
- Ubuntu(所有桌面版):`fuse3` 作为 `gvfs-fuse` 的依赖项安装,属于 `ubuntu-desktop` 元包的一部分。
- Fedora Workstation:`fuse3` 默认安装用于 GNOME、Toolbox 和 Open VM Tools。`fusermount3` 作为 setuid-root 二进制文件存在于默认安装中。
- Linux Mint:基于 Ubuntu,通过相同的 `gvfs-fuse` 依赖链继承 `fuse3`。
- 任何基于 GNOME 的桌面发行版:`gvfs-fuse` 依赖于 `fuse3`,因此任何发行 GNOME 桌面的发行版都向非特权用户提供 `fusermount3`。
setuid 位仅用于初始的 `mount(2)` 调用。易受攻击的代码路径稍后才会进入,此时用户控制的守护进程会响应内核的 `READDIR` 请求,提供一个精心构造的 dirent。辅助程序的加固不会造成任何阻碍。
### 路径 B:非特权用户命名空间(无需 fusermount3)
自 Linux 4.18 起,非特权用户可以创建用户 + 挂载命名空间 (`unshare -Ufirmp`),在其中获得 `CAP_SYS_ADMIN`,打开 `/dev/fuse`(通常模式为 0666),并直接挂载 FUSE 文件系统,完全不需要 `fusermount3`。readdir 溢出会全局破坏内核页面缓存内存,无论命名空间隔离如何。此路径适用于任何允许创建非特权用户命名空间的发行版:
- Debian (Bullseye 及更高版本):自 Debian 11 起,`kernel.unprivileged_userns_clone` 默认启用。
- Arch Linux:标准内核 (`linux`, `linux-lts`, `linux-zen`) 上默认启用。
- RHEL 9 / CentOS Stream 9 / Rocky / Alma:`user.max_user_namespaces` 默认为非零值 (~14803);非特权用户命名空间默认启用。
- Fedora(同样由路径 A 覆盖在 Workstation 上):默认启用。
两条路径的组合覆盖了所有主要 Linux 发行版的默认配置。
### 为什么所有缓解措施都无效
**AppArmor。** Ubuntu 24.04 LTS 增加了对非特权用户命名空间的限制,但 `fusermount3` 在 24.04 上不受限制,因此路径 A 不受影响。Ubuntu 24.10 及更高版本为 `fusermount3` 本身附带了 AppArmor 配置文件 (`/etc/apparmor.d/fusermount3`),但该配置文件是一个基于路径的允许列表,列出了允许的挂载目标。它明确允许在 `$HOME` 和 `$XDG_RUNTIME_DIR` (`/run/user/$uid/`) 下的 FUSE 挂载,因为 GNOME、GVFS、SSHFS、Flatpak、AppImage、Docker-with-FUSE 和基于 fstab 的 FUSE 挂载都需要它。漏洞利用对挂载位置不敏感;当守护进程的回复被处理时,溢出发生在内核页面缓存内存中,因此配置文件允许的任何挂载路径都足以触发它。收紧配置文件以拒绝所有用户可写的挂载目标将会破坏 Ubuntu 桌面。2025 年期间 fusermount3 配置文件变更的趋势始终是朝着*更宽松*的方向发展,因为合法的工作流程不断被破坏。
**SELinux。** RHEL 和 Fedora 上的默认策略不会通过 `fusermount3` 或用户命名空间限制非特权 FUSE 挂载。
**seccomp。** 不适用于常规用户会话。
唯一能真正阻止该漏洞利用的配置是删除 `fusermount3` 的 setuid 位和禁用非特权用户命名空间,但这两者都会破坏合法功能(Snap、GVFS、SSHFS、无根容器)。没有主流发行版会提供这样的配置。
因此威胁模型是强大的:默认桌面上的普通登录用户。如果你能挂载一个 FUSE 文件系统(而你能),你就可以提供触发漏洞的 dirent。
## readdir 缓存
当使用 `FOPEN_CACHE_DIR` 打开 FUSE 目录时,内核会将守护进程的目录列表缓存到附加到 inode 的页面中,这样后续的 `getdents` 调用无需往返用户空间即可服务。来自守护进程的回复是一个打包的 `struct fuse_dirent` 记录流:一个固定的 24 字节头部(inode、偏移量、namelen、type),后跟 `namelen` 字节的名称,而 `fuse_add_dirent_to_cache()` 将每个记录复制到当前的缓存页中。每个记录的序列化大小为 `reclen = FUSE_DIRENT_SIZE(d)`,即 `FUSE_REC_ALIGN(FUSE_NAME_OFFSET + namelen)`,头部加上名称,并向上舍入到 8 字节边界。
缓存是一个在运行偏移量处写入的 `PAGE_SIZE` 页面序列,而该函数做出的唯一布局决策是下一个记录是否适合当前页的剩余空间。
## 漏洞:从未检查完整记录的检查
简化后的复制过程如下:
```c
/* fuse_add_dirent_to_cache(), 简化版, 修复前 */
reclen = FUSE_DIRENT_SIZE(dirent); /* FUSE_REC_ALIGN(FUSE_NAME_OFFSET + namelen) */
if (offset + reclen > PAGE_SIZE) { /* 它适合当前页的剩余空间吗? */
index++; /* 不适合:前进到一个新页面 */
offset = 0; /* 并从新页面的顶部开始 */
}
addr = kmap_local_page(page[index]) + offset;
memcpy(addr, dirent, reclen); /* reclen 从未与 PAGE_SIZE 本身进行比较 */
```
该检查回答了一个问题(“此记录是否适合剩余空间?”),并通过移动到新页面并将 `offset` 重置为 0 来处理“否”的情况。只要记录在独自占用一个页面时总能适合该页面,这样做就是正确的。该函数只是假设了这一点。它从未询问 `reclen` 是否超过 `PAGE_SIZE`,因此当 `offset` 重置为 0 并且 `memcpy` 运行时,大于页面的记录会直接从新页面的末尾写入。当 `namelen` 达到协议当前允许的最大值时,恰好会发生这种情况:
```c
reclen = FUSE_REC_ALIGN(FUSE_NAME_OFFSET + namelen)
= FUSE_REC_ALIGN(24 + 4095)
= 4120
overflow = 4120 - 4096 = 24 字节
```
溢出的 24 个字节是 `dirent->name[]` 的尾部,守护进程可以完全控制它。因此,这是一个线性的、固定大小的、完全可控的写入,从一页的末尾写入下一页的开头。
### 24 字节写入超过页面末尾
`namelen=4095` 使得记录比其被复制到的页面还大。未按比例绘制。
```
reclen = FUSE_REC_ALIGN(24 + 4095) = 4120 B
readdir 缓存页 4096 B | 相邻内核页 (页缓存, 例如 /etc/passwd)
PAGE_SIZE = 4096 | 24 B
当记录不适合剩余空间时,检查会前进到新页面;但它从未拒绝不能适合*任意*单个页面的记录。大于页面的记录被复制到新页面的偏移量 0;尾部溢出到下一页。
```
## 潜伏六年:漏洞如何被武装
这个漏洞的有趣之处在于,缺失的检查并不新鲜。自从引入 readdir 缓存以来,它就一直存在错误。它只是六年来一直无法触达,并且被一个与其无关的提交悄悄地武装了起来。
缓存以及这个完全相同的“适合剩余空间”检查是在提交 `69e34551152a`(“fuse: allow caching readdir”,Miklos Szeredi,2018 年 10 月,首次出现在 v4.20)中引入的。当时 `FUSE_NAME_MAX` 是 1024,因此守护进程能产生的最大记录是 `FUSE_REC_ALIGN(24 + 1024)` = 1048 字节,大约是页面的四分之一。记录永远无法接近 `PAGE_SIZE`,因此缺少 `reclen > PAGE_SIZE` 检查无关紧要。这个假设(“任何单个记录都适合一个页面”)当时成立,只是从未被明确写下。
这个假设在提交 `27992ef80770d`(“fuse: Increase FUSE_NAME_MAX to PATH_MAX”,Bernd Schubert,2024 年 12 月,首次出现在 v6.15)中被打破。该更改为每个连接添加了 `fc->name_max`,初始化为旧的 1024,并在 `FUSE_INIT` 协商后(一旦守护进程通告 `max_pages > 1`)提升到 `PATH_MAX - 1` (4095)。由于守护进程是攻击者控制的,这个门禁形同虚设。该提交尽职尽责地更新了 `fuse_notify_inval_entry`、`fuse_notify_delete` 和 `fuse_lookup_name` 中的长度检查以使用新的限制,但它从未触及 `fs/fuse/readdir.c`,因为没有人将名称长度上限与相隔三个文件的 readdir 缓存页面边界检查联系起来。随着上限的提高,`reclen` 达到了 4120,2018 年的潜伏漏洞变成了一个 24 字节的可控溢出。
### 潜伏六年,被一个无关的提交武装
缺失的检查于 2018 年引入,但直到 2024 年 FUSE_NAME_MAX 被提高后才变得可触达。
```
Oct 2018 · v4.20 | Dec 2024 · v6.15 | Apr 2026
69e3455 | 27992ef | 51a8de6
allow caching readdir: | raise FUSE_NAME_MAX to PATH_MAX. | reject oversized dirents.
添加了缓存 + 检查。| name_max 4095. 未触及 readdir.c。 | 添加 reclen > PAGE_SIZE。
NAME_MAX 1024。 | | 修复: S. Page.
潜伏: 最大 reclen ≈ 1048 B ≤ 页面 | 武装: 最大 reclen 4120 B > 页面 | 已修复
潜伏/不可触达 | 可触达/可利用 | 已修复
在一个文件中提高名称长度上限,将另一个文件中存在六年的检查变成了页面溢出。
```
## 为什么 sanitizer 保持沉默
对于内核堆溢出,第一反应是使用 KASAN,但这里错了。KASAN 使用中毒的 redzone 和释放后分配的 quarantine 来保护 slab 对象和栈帧:溢出 `kmalloc` 对象末尾会触发 redzone 并得到清晰报告。此溢出两者都不是。readdir 缓存页面来自页分配器,直接映射中紧随其后的字节只是下一个页面,不是 slab 对象,不是 redzone,KASAN 不检测任何东西。24 字节写入没有越过 sanitizer 正在监视的任何边界,因此它什么也不报告。该漏洞既安静又有用,原因相同。针对 KASAN 内核模糊测试 FUSE 守护进程不会标记它。第一个可见症状是下游的:损坏的邻居,如果相邻页面是内核需要的东西则会出现无法解释的 oops,或者如果相邻页面是缓存的文件数据并且您只更改了其内容,则什么也不会发生。页面缓存页面是有效的目标,重写一个页面会重写内核将继续信任的文件内容。
## 利用
此漏洞提供了一个受限的原语:正好 24 个字节,位于固定偏移量(物理上紧随缓存页面的页面的字节 0),内容完全由守护进程控制。这不是 write-what-where。无法控制长度,无法自由放置于受害者页面内,也无法跳过页面。这是一个从一页线性溢出到下一页的漏洞,它是否有用完全取决于该下一个物理页面帧中放置的是什么。
### 不同类型的溢出
这里的利用形态从根本上不同于 slab 溢出,差异决定了后续的一切。
在 slab 溢出(更常见的情况)中,您会写入一个 `kmalloc` 对象的末尾,进入同一缓存中下一个对象的头部或主体。受害者是一个结构体,游戏是类型混淆:您选择一个位于相同大小类别的目标结构体,将其喷射到相邻槽位,并破坏特定字段(函数指针、引用计数、标志字)。分配器的空闲列表顺序是您的杠杆,您的工具(KASAN、SLUB 调试、空闲列表随机化)正是为此场景设计的。
这里溢出根本不落在 slab 中。readdir 缓存页面是纯页分配器页面 (`alloc_page(GFP_KERNEL)`),从末尾溢出的 24 字节落在内核直接映射中下一个物理页面帧号上的任何页面。那不是带有 redzone 的 slab 对象;它是一个原始的 4096 字节页面,可以是任何东西:支持文件的页面缓存页面、属于进程的匿名页面、页表页面、伙伴列表上的空闲页面,或者从外部而非内部查看的另一个 slab 页面。KASAN 不检测页分配器页面之间的间隙,因此 sanitizer 无法发现溢出。并且目标不是对象内已知偏移处的结构体字段。它是整个页面的前 24 字节,意味着所有从该页面基地址开始的数据结构都会将其头部覆盖。
因此这里有趣的目标不是内核结构体,而是**内核页面**,而最有意义的内核页面是那些覆盖字节 0 会产生内核将对其采取行动的语义效果的页面。这直接指向页面缓存。
### 为什么页面缓存是自然目标
页面缓存是内核文件数据的内存副本。当进程读取 `/etc/passwd` 时,内核将块从磁盘读入页面缓存页,此后每个读取同一文件的进程(包括内核本身在执行 `su` 和 PAM 认证时)都从该缓存页读取。该页面不为每个读取者复制;有一个共享副本,其内容在被逐出或通过文件系统层写入文件之前被视为权威且受信任。破坏页面缓存页不会写回磁盘。不会触发任何文件系统权限检查,因为损坏从未通过 `write(2)` 或 VFS。不会将页面标记为脏,因为修改是通过直接映射别名完成的,而不是通过文件系统的地址空间
相似文章
Lobsters Hottest
DRM GEM核心ioctl DRM_IOCTL_GEM_CHANGE_HANDLE中存在一个释放后使用(use-after-free)漏洞,允许具有渲染节点访问权限的非特权本地用户提权至root。该漏洞已于2026年5月在Linux内核主线中修复。
Lobsters Hottest
CVE-2026-31431(Copy Fail)是Linux内核中的一个本地提权漏洞,影响自2017年以来的所有主流发行版,允许非特权用户通过AF_ALG加密子系统对任何可读文件的页缓存进行确定性的4字节写入,从而获得root shell访问权限。
Ars Technica
Linux内核中一个错误的字符引入了一个use-after-free漏洞(CVE-2026-53111),允许非特权用户在Debian和Ubuntu系统上将权限提升至root;该漏洞已修复并移植回旧版本。
Lobsters Hottest
Qualys和Anthropic披露了CVE-2026-64600,这是Linux内核XFS文件系统中的一个竞态条件漏洞,允许本地权限提升至root,影响超过1600万个系统,且无内核日志输出,能够在重启后持续存在。
Lobsters Hottest
FreeBSD中存在一个严重的本地权限提升漏洞(CVE-2026-45257),允许无特权用户将任意数据写入任何可读文件的页面缓存,绕过文件权限和标志,最终导致完全获取root权限。该漏洞影响FreeBSD 13.0及更高版本的默认安装,通过sendfile、KTLS和内核内AES-GCM解密的不安全组合实现。