关注了就能看到更多这么棒的文章哦~
FUSE folio conversion confusion
By Daroc Alden
February 18, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1008714/
内核开发者们一直在努力将各种内部接口转换为使用 folios(页集);虽然这个过程一直在进行,但由此引入的回归(regression)问题仍然时有发生。在 2024 年 12 月,人们发现安装 Flatpak 应用可能会触发内核中的一个文件系统漏洞,导致软件从磁盘读取错误的数据。这个问题很快得到了修复,但仅仅没过多久,由于页集重写而导致的另一个问题又在同一个内核子系统中冒了出来。这个问题是由一位 Arch Linux 用户发现的,他注意到在 Flatpak 应用中选择文件会导致内核崩溃。现在,这两个漏洞都已经修复,但可能还存在更多未被发现的 bug。
Flatpak 使用 用户空间文件系统(Filesystem in Userspace,FUSE)为打包的应用创建隔离的文件系统。特别是,Flatpak 允许应用访问沙箱外部的特定文件或目录就是通过这种方式实现的。XDG Desktop Portal 服务接受用于诸如打开文件等操作的 D-Bus 请求,并向用户展示一个对话框,允许他们选择授予程序何种访问权限。然后,Flatpak 使该文件在应用的挂载命名空间中可用,并使用 FUSE 将文件系统操作转发到基础文件。
去年 10 月,Joanne Koong 转换了 FUSE 代码,使其在 direct I/O 中使用页集。当时,FUSE 代码期望其处理的所有页集都是很小的 —— 当这些页集来自页面缓存(page cache)时,情况确实如此。但直接 I/O(direct I/O)则不同。由于用户空间程序控制着直接 I/O 请求的大小,因此与它们关联的页集可能会非常大。在 Koong 转换了 FUSE 代码后,它最终错误地计算了内存中请求的偏移量,从而返回了错误的数据。实际上,代码计算的是相对于页集开始处的偏移量,但却将其视为相对于特定页面的偏移量。具有多个页面的页集会导致从内存中的错误位置读取数据。
Malte Schröder 在 12 月初 报告了这个问题,最初指出 一个特定的合并提交引入了该问题。在花费更多时间对内核进行二分查找(bisecting)后,他确定 Koong 的提交才是引入问题的那个。
为了回应 Bernd Schubert 请求提供重现该问题的方法,Schröder 表示,通常可以通过设置一个配置了 bcachefs 作为根文件系统的 Arch Linux 虚拟机来重现该问题,然后尝试安装 FreeCAD Flatpak。“这是一个非常奇怪的测试,但我没有找到更具体的重现方式”,他说。
最终,Josef Bacik 确定了代码中处理大型页集不正确的部分。Matthew Wilcox 同意他的评估,因此 Bacik 提供了一个补丁,但 Schröder 报告说,该补丁并没有解决问题。Koong 发现了 Bacik 补丁的一个问题,并找到了 一个更简单的测试用例来复现这个 bug。
然而,Schröder 的问题并没有完全通过建议的修复得到解决。最终,Flatpak 能够(表面上)正常工作,是结合了 Bacik 的补丁、Koong 的修复以及 Kent Overstreet 的树中对 bcachefs 快照的相关更改的结果。为解决方案做出贡献的人数之多,在某种程度上证明了开源的力量,同时也证明了内核中文件系统代码的复杂性。Bacik 称这是“测试的失败”,这并不是为了贬低 Koong 的工作,而是为了强调扩展 Linux 文件系统测试的重要性:
我是说,我们作为一个文件系统社区,忘记了 fstests 并不像我们想象的那么详尽。老实说,我很惊讶地发现,我们实际上没有任何东西会专门分配大型页集,然后对它们进行 O_DIRECT 操作。现在我们知道了,Joanne 已经有了后续行动,以扩展 fsx,专门分配大型页集,这样我们就可以对这种情况进行测试覆盖。
进一步的问题
但是,Koong 添加的测试并没有充分证明能够捕获将 FUSE 代码移植到使用页集所引入的所有问题。2 月 6 日,Christian Heusel 分享了来自 Arch Linux 用户的 报告,报告称尝试从内核版本 6.13 上的 Flatpak 应用打开文件可能会导致内核崩溃。Heusel 提到了之前关于 FUSE 问题的讨论,称 Arch 用户已经尝试了其中包含的修复程序,但没有奏效。最终,他将问题归咎于来自 Bacik 的 一个提交,该提交将 FUSE 转换为使用页集进行预读(readahead)。
Miklos Szeredi 认为问题与如何为操作分配页面有关,但 Wilcox 不同意,称这个问题“非常奇怪”。Vlastimil Babka 建议问题是一个释放后使用(use-after-free)的 bug。事实证明确实如此,但找到引入问题的位置变得更加困难。他最终 确定这很可能是一个缺失的对 folio_put()
的调用,但不知道在哪里修复它。
Bacik 表示赞同,他说:“我也很困惑这里什么是正确的事情。”他推测在预读请求挂起时,可能需要锁定页集,而不是持有对它的引用。Bacik 询问 Wilcox(作为页集的设计者),在这种情况下,他期望的操作顺序是什么。
但 Wilcox 表示这不可能是问题,因为此时页集 已经 被锁定 —— 因此页面缓存将持有引用。然而,Babka 能够生成 一个补丁,该补丁移动了一个 folio_put()
调用,并根据 Koong 的说法 修复了问题。Wilcox 推测实际上是多了一个 folio_put()
调用,并提出了 FUSE 代码中的一个候选者。额外的 folio_put()
调用会将页集的引用计数递减为零,然后当锁被释放时,页集将被释放,而内核的某些其他部分仍然持有对它的引用。
Koong 制作了 一个替代补丁,该补丁似乎也修复了该 bug。Babka 指出,尽管这两个补丁基本上已经确认某个地方的 folio_put()
调用存在问题,但实际上都没有帮助缩小哪个调用是不正确的范围。
在调试会话中,Koong 发现证据表明她的补丁是正确的,但 Bacik 分享了 一个单行补丁,删除了在某些不同代码解锁它正在处理的页集之后的一个 folio_put()
调用。审查 Bacik 的补丁的过程导致 Wilcox 注意到在建议的更改之上几行代码中,可能存在对 不同 页集的引用被丢弃的情况。但 Jeff Layton 认为现有代码实际上是正确的。Koong 不同意;她认为现有代码正在泄漏一个引用,并且通过稍微重构代码,使每个对页集的引用的预期生命周期更加明确会更好。
她的论点 说服了 Layton,他唯一的建议是添加一条注释,希望使这种情况对未来的审查者更加清楚。Babka 希望避免在 Koong 的最终版本中添加不必要的引用,但除此之外,他同意她是正确的。Koong 认为带有额外引用的版本更具可读性,但同意按照 Babka 推荐的方式进行操作,以便看到整个事情结束。
讨论和 Koong 的补丁的结合使问题的根本原因变得清晰:不同的开发者对页集是否受到锁定或引用计数(也许是为了进行性能优化,以避免对引用计数进行不必要的更改)的保护有不同的想法。因此,在某个时候, folio_get()
和 folio_put()
调用不再匹配。这并不明显,因为更改引用计数并不是绝对必要的,只要页集被锁定即可。并且由于同样的原因,它不会导致立即崩溃 —— 但一旦页集被解锁(unlock),它就会导致延迟崩溃。
Koong 提交了对 Szeredi 的树的修复。从那里,它应该按照正常流程进入主线(mainline)内核。在许多方面,整个事件的过程展示了内核开发正确运作的一幅图景 —— 几位开发者一直在努力使一个子系统更高效、更易于维护,这是清理内核的长期项目的一部分。在工作中,他们不可避免地引入了一些 bug。这些 bug 很快被滚动发布(rolling-release)版本的用户注意到,他们对 Git 历史进行了二分查找,以找到最初引入它们的精确提交。他们将问题报告给了内核邮件列表,在那里,一组对相关代码和更广泛的内核设计都很熟悉的专家聚集在一起,友好地合作,以确定根本原因。最终,产生了一个所有专家都可以同意的补丁,并且这些 bug 将很快得到修复。
但尽管最终取得了成功,但由于围绕何时需要递增引用计数、哪些引用仍然有效、底层页集的大小以及诸如此类的几个细节的不确定性,该过程无疑被拖慢了。页集是内核的核心数据结构;当转换完成后,每个子系统都将使用它们来管理页面。如果使用它们的 API 过于复杂,以至于十位专家花费了如此多的精力来尝试追踪相对简单的错误,并且文件系统测试不够彻底,无法提前发现问题,那么也许值得考虑对工具或代码进行哪些更改可以帮助识别 API 的误用,以避免它们成为问题。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~