关注了就能看到更多这么棒的文章哦~
A review of file descriptor memory safety in the kernel
By Daroc Alden
August 22, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/985853/
2024 年 7 月 30 日,Al Viro 向 linux-fsdevel 邮件列表发送了一个包含补丁集的邮件,并在邮件中附上了一份全面的说明信,解释了他最近在确保内核内部对文件描述符的表示在内核中被正确使用方面所做的工作。文件描述符无处不在;许多系统调用都需要处理它们。Viro 的审查发现了一些现有的错误,并且可能会防止将来出现更多类似错误。他还提出了一些建议,可以帮助保持内核中对文件描述符的使用一致。
用户空间中文件描述符用非负整数表示。在内核中,这些实际上是进程文件描述符表中的索引,该表存储类型为struct file
的条目。大多数接收文件描述符的系统调用(除了dup2()
之类的调用之外,因为它们只触及文件描述符表,不触及文件本身)都需要引用关联的 struct file
来确定如何处理该调用。这些结构与内核中的许多其他内容一样,都是引用计数的。
因此,典型的系统调用可能会增加文件对象的引用计数,对其进行一些操作,然后再次减少引用计数。这将是可行的,但是,正如 Viro 在他的说明信中所说,“增加和减少引用计数需要开销,对于某些应用程序来说,这开销不可忽略。”因此,实际上,内核只在知道描述符表与其他线程共享时才会更改引用计数。如果描述符表未共享,内核知道引用计数不会改变,并且文件不会被释放,因此没有理由耗费这种额外开销。
不幸的是,这会使处理这些结构变得复杂,因为线程 可能会被创建 could end,这可能发生在检查表是否共享和系统调用结束之间。因此,内核需要存储它是否跳过了引用计数的增加。某些文件操作也需要对文件加锁,因此内核还需要记住它是否同时加了锁。这两个信息都与指向 struct file
的指针一起存储在struct fd
中,这是 Viro 补丁集的主要主题。
struct fd
的实例主要由特殊的辅助函数创建和销毁:fdget()
和 fdput()
。Viro 的调查涉及了内核中的所有 160 个调用站点,包括一些没有经过辅助函数的站点,并发现了几个内存泄漏和使用后释放错误。“这类问题不断出现;如果能够尽可能自动完成,那就太好了。”
总的来说,Viro 的报告表明内核处于良好的状态。大多数使用 struct fd
的地方都没有问题。其中一个例外是 overlayfs,它使用 fd
结构时不仅表示“非引用计数文件”、“引用计数文件”或“空”,还表示各种错误。 struct fd
不适合存储错误,overlayfs 的这种用法阻碍了 Viro 的一些后续工作。由于这是唯一以这种方式对待该结构的代码,Viro 建议 overlayfs 可能希望切换到另一个类型,该类型对表示错误有更好的支持。
几乎所有其他地方,内核都在单个函数内获取和释放 struct fd
,这使得审计代码以确保使用正确变得很容易。但是,手动检查如此多的使用情况很繁琐;Viro 在他的说明信中最后探讨了编译器如何帮助进行这种检查。内核已经在几个地方使用__cleanup
属性 来管理函数局部变量,因此如果该属性也可以在这种情况下使用,那将是理想的。不幸的是,这样做并不简单 - 至少,如果人们也希望获得良好的代码生成。
具体来说,使用当前的 struct fd
表示,用于构建内核的编译器不够智能,无法判断使用空 struct fd=(不指向 struct file 的 struct fd)调用 =fdput()
是一个空操作,可以省略。这会添加一些不必要的代码 - 主要用于错误路径,但它仍然会造成性能损耗。使用 __cleanup
会使这个问题更加严重,因为它会在人类可以判断为不必要但编译器无法判断的分支中添加清理代码。Viro 说 Linus Torvalds 建议将该结构改为单个指针大小的整数,并将标志打包放到最低位。这种表示将允许编译器更准确地确定何时可以省略对 fdput()
的调用。
然而,这并不是使用 __cleanup
的唯一障碍。该属性与 goto
语句的交互效果很差;在 GCC 12 版本及更早版本中,使用 goto
语句跳过带注释变量的初始化仍然会导致调用清理代码,但它会引用未初始化的变量,这通常会导致不良影响。Clang 会捕获此问题,“但我们既不能强制执行它(并非所有目标都支持,首先),也不能将最小 gcc 版本从 5.1 全部提升到 13。”
因此,虽然大多数函数可以使用 __cleanup
,但有些函数需要更手动的方法。Viro 补丁集的大部分内容包括针对不同子系统的补丁,这些补丁将 struct fd
的使用转换为尽可能使用 =__cleanup=。Viro 使用 Clang 验证了支持以这种方式构建的代码部分,并手动检查了其余部分。
审核
审阅者对该补丁集总体上感到满意。Christian Brauner 说 这个补丁集整体上“看起来不错,而且很简单…没什么真正令人惊讶的地方。” 与任何大型补丁集一样,有一些地方的补丁与其他正在进行的更改冲突,但这在没有大惊小怪的情况下就得到了解决。
Andrii Nakryiko 和其他 BPF 开发人员对 Viro 在 BPF 代码中的方法有一些评论,建议进行少量重构可以使必要的更改不那么侵入。Nakryiko 最终通过 bpf-next 树提交了一个单独的补丁集,其中包含这些更改。
总的来说,Viro 的补丁集并没有特别令人兴奋或有争议 - 这是件好事。虽然 LWN 通常报道内核开发过程中最有争议的部分,但重要的是要记住,绝大多数更改都像这一样(尽管可能范围更小)。一旦该补丁集被合并,用户将能够享受内核中更少的“使用后释放”漏洞,内核开发人员将能够享受围绕文件访问的更简单、不易出错的代码。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~