CVE-2026-31694:Linux内核中FUSE目录页缓存溢出漏洞分析
漏洞分析
结合源码详细分析 Linux 内核中 FUSE 目录页缓存溢出高危本地提权漏洞 CVE-2026-31694
背景知识 ---- ### [](http://localhost:4000/2026/07/08/CVE-2026-31694/#FUSE-%E5%AD%90%E7%B3%BB%E7%BB%9F "FUSE 子系统")FUSE 子系统 **FUSE(Filesystem in Userspace,用户空间文件系统)** 是类 Unix 操作系统(如 Linux、macOS 等)提供的一个软件接口。它允许没有 root 权限的普通用户在用户态创建和挂载自定义文件系统,而无需深入修改内核源码。 传统的文件系统(如 Ext4)直接在内核中实现,开发调试困难且需要管理员权限。 一个进程可以对一个FUSE的目录执行“列目录”操作,如 ls、readdir()、getdents()等,此时内核并不知道这个目录里有哪些文件,所以它会去查询用户态 FUSE 服务端。 **/dev/fuse** Linux 内核中实现了一个名为 /dev/fuse 的字符设备接口,用户态进程打开这个设备之后,就可以通过 read()/write() 或 io\_uring 和内核交换 FUSE 协议消息。 **FUSE daemon** 则是运行在用户态的 文件系统服务进程。 具体访问过程如下: - 应用程序通过 open/read/write/readdir/stat 等系统调用 对FUSE文件系统的文件进行操作;(和其他文件系统的操作接口保持一致); - 内核 VFS + FUSE 模块发现目标文件是FUSE文件系统的文件,就包装形成FUSE 协议请求/响应发送给/dev/fuse, /dev/fuse 通过read/write或io\_uring和FUSE daemon交互,触发相应的回调函数; - daemon 打开 /dev/fuse,读取内核请求,调用 read/write或io\_uring 相关的回调函数,返回文件属性、目录项、文件内容或错误码;并将相关的结果通过/dev/fuse交给内核; - 内核接收到结果之后,解析daemon返回的结果,并将其转换成 Linux VFS 的结果返回给用户态进程。 下面通过一个例子理解具体流程: 1. 进程调用 getdents() 读取目录项。 2. 内核发现这是 FUSE 文件系统目录。 3. 内核通过 /dev/fuse 给 **FUSE daemon** 发请求,请求大致可以分为如下两种类型: - FUSE\_READDIR:只要目录项基本信息。 - FUSE\_READDIRPLUS:目录项加上 inode 属性信息,类似顺便做 lookup/stat。 4. FUSE daemon 返回一段字节 buffer。 5. 这段 buffer 里面连续存放多个 **fuse\_dirent** 记录。 6. 内核解析这些记录,再把目录项返回给调用者。 大致的流程图如下:  ### [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E7%9B%B8%E5%85%B3%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84 "相关数据结构")相关数据结构 fuse\_dirent 是 FUSE 协议里表示一个目录项的结构,即目录遍历结果中的一条记录。 fuse\_dirent结构体的定义如下: ```c struct fuse_dirent { uint64_t ino; // 目录项 inode 号 uint64_t off; // 下一个 readdir 位置 uint32_t namelen; // 文件名长度,来自 FUSE 服务端,漏洞核心输入 uint32_t type; // 文件类型 char name[]; // 变长文件名 }; ``` 其中 name\[\] 是变长文件名,比如: \[dirent: name=”a”\] \[dirent: name=”hello.txt”\] \[dirent: name=”very\_long\_filename”\] 在64位机器上,char name\[\] 成员在fuse\_dirent结构体中的偏移通常是24. ### [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E7%9B%AE%E5%BD%95%E7%BC%93%E5%AD%98 "目录缓存")目录缓存 两种和目录相关的缓存: - dentry cache :缓存路径查找结果,用于加速通过文件路径 快速定位 inode; - directory page cache :缓存目录文件内容,一串目录项字节;用于加速 ls/getdents() ; FUSE daemon 允许内核缓存目录遍历结果(即上述第二种),FUSE daemon 在响应 打开目录 的请求时,可以通过 `FOPEN_CACHE_DIR` 标志通知内核该目录的 readdir 结果可以缓存。 由于目录里可能有很多项,包括很多文件、子目录信息,所以 daemon 返回给内核的是一段连续字节: ```none [fuse_dirent for a.txt][fuse_dirent for b.txt][fuse_dirent for subdir] ``` 后续 getdents() 内核发现目录 readdir cache 有效,会直接从目录 inode 的 page cache 取出 fuse\_dirent 字节, 解析后返回给用户进程。 ### [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E7%9B%AE%E5%BD%95%E7%BC%93%E5%AD%98%E7%9B%B8%E5%85%B3%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84 "目录缓存相关数据结构")目录缓存相关数据结构 [https://elixir.bootlin.com/linux/v7.0/source/fs/fuse/fuse\_i.h#L117](https://elixir.bootlin.com/linux/v7.0/source/fs/fuse/fuse_i.h#L117) - rdc.cached:readdir cache 是否已经完整构建。 - rdc.size:目录项缓存当前写了多少字节。 - rdc.pos:缓存末尾对应的目录 offset,也就是下一条 dirent 应该接在什么位置。 - rdc.version:缓存版本,用来识别缓存是否被失效/重建。 - rdc.lock:保护上述 readdir cache 状态。 file->f\_mapping 是当前打开文件/目录的 page cache 容器;FUSE 用它来保存目录项缓存页。struct file中有一个名为f\_mapping的成员,其类型是address\_space:  struct address\_space 的定义如下:  成员i\_pages是 page cache 容器,用 xarray 存 folio/page,key 通常是页索引。 ### [](http://localhost:4000/2026/07/08/CVE-2026-31694/#kmap-local-page "kmap_local_page")kmap\_local\_page <https://elixir.bootlin.com/linux/v7.0/source/include/linux/highmem-internal.h#L71> 该函数返回一个虚拟地址,用于访问struct page \*page指向的内存页:  该函数主要调用\_\_kmap\_local\_page\_prot函数:  64位情况下通常config是CONFIG\_HIGHMEM=n ,则一定会调用 page\_address(page),返回的是对应的 struct page \*page 对应的线性映射区地址。 [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90 "漏洞分析")漏洞分析 ---------------------------------------------------------------------------------------------------- 这个漏洞发生在内核将FUSE daemon 返回的目录项缓存进目录 page cache的过程中。 前面介绍过fuse\_dirent是一个变长结构体,其长度由 namelen 决定: ```c struct fuse_dirent { uint64_t ino; // 目录项 inode 号 uint64_t off; // 下一个 readdir 位置 uint32_t namelen; // 文件名长度,来自 FUSE 服务端,漏洞核心输入 uint32_t type; // 文件类型 char name[]; // 变长文件名 }; ``` 而namelen以及name\[\]中的内容都是由FUSE daemon提供,然后内核解析。 内核用如下宏算序列化长度: FUSE\_DIRENT\_SIZE = align\_8(FUSE\_NAME\_OFFSET + namelen) 其中 FUSE\_NAME\_OFFSET 是 24 字节。 daemon 提供的 namelen = 4095, 那么 24 + 4095 = 4119 align\_8(4119) = 4120,大于一个页的长度(4096字节)。 漏洞发生在fuse\_add\_dirent\_to\_cache()函数,该函数的功能是把从 FUSE daemon 接收的 use\_dirent追加写入到该目录 inode 的 readdir page cache中。 源码链接如下: <https://elixir.bootlin.com/linux/v7.0/source/fs/fuse/readdir.c#L32> 该函数首先获取当前目录的fuse\_inode,然后通过 FUSE\_DIRENT\_SIZE宏计算这条目录项的序列化长度:  之后检查当前 readdir cache 是否还能追加,fi->rdc.cached表示该目录的 readdir cache 是否已经完整构建完成:  根据当前缓存大小 fi->rdc.size 算出写入位置,如果当前 page 剩余空间放不下这条 dirent,就跳到下一个 page 开头(FUSE readdir cache 有个隐含约束 ——一条 fuse\_dirent 不能跨 page):  一个目录的readdir page cache可能包含多个内存页面,fi->rdc.size表示当前目录的readdir page cache中已经写入的数据,index是size >> 13,用于计算当前要写readdir page cache的第几个页,offset则是计算出来新写入的数据在内存页面中的偏移,之后通过offset+reclen计算当前页面中剩余的空间是否够放得下当前的信息,如果放不下,就index++换下一个页面,offset被设置成0。 接下来找到或创建该目录 inode 对应的 page cache 页,如果offset为0则说明用一个新的页,反之则用已有的页: - find\_lock\_page(mapping, index) 函数的功能是在 mapping 的 page cache 里查找第 index 页;如果找到了,就把这个 page 锁住并返回;如果没找到,返回 NULL;该函数不会创建新 page。 - find\_or\_create\_page(mapping, index, gfp) 函数的功能是先在 mapping 的 page cache 里查找第 index 页,如果存在,锁住并返回;如果不存在,就分配一个新 page,插入 mapping 的 page cache,锁住并返回;失败则返回 NULL。  接下来要将这条 fuse\_dirent 的原始字节复制进 page cache,这里最远写入的位置是addr+offset+reclen,而前面检查`if (offset + reclen > PAGE_SIZE) {`只是长度如果过大就index++和offset=0启用新的一页,但是并没有考虑到新的一页还是不够的情况,因此会发生溢出:  kmap\_local\_page在通常情况下会返回page在线性映射区对应的虚拟地址,因此如果能够做好相应的page风水,将特权文件的页缓存命中在当前目录readdir页面缓存的下方,就有可能实现越权写入。 最后更新 readdir cache 状态:  [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E8%A1%A5%E4%B8%81 "补丁")补丁 ------------------------------------------------------------------------------ 该漏洞对应的补丁如下: <https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=474ce83c96a55f2eeb14dee2be375eeadfdacdf5>  补丁的功能是如果本次目录缓存内容的长度超过一个页大小则直接退出,避免出现溢出。 [](http://localhost:4000/2026/07/08/CVE-2026-31694/#%E5%8F%82%E8%80%83 "参考")参考 ------------------------------------------------------------------------------ <https://zhuanlan.zhihu.com/p/106719192> <https://nvd.nist.gov/vuln/detail/CVE-2026-31694> <https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=474ce83c96a55f2eeb14dee2be375eeadfdacdf5>
发表于 2026-09-07 10:04:39
阅读 ( 5244 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
q1ming
2 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!