LWN: 放开kcmp()!
关注了就能看到更多这么棒的文章哦~
kcmp() breaks loose
By Jonathan Corbet
February 11, 2021
DeepL assisted translation
https://lwn.net/Articles/845448/
Linux 内核实现了大量的系统调用,所以对于大多数人来说总是会有一些不熟悉的系统调用,毕竟不是每个人都需要知道 setresgid()、modify_ldt()或 lookup_dcookie()的细节的。但是,哪怕是对 Linux 系统调用列表有着非常深入了解的那些开发者,也会对 kcmp()感到陌生,因为在 kernel 编译中缺省是关闭的。不过,现在似乎更多人知道了 kcmp,人们也在努力让 kcmp() 能更广泛的使用起来。
kcmp()系统调用是在 2012 年添加的,目的是为了解决用户空间中的 checkpoint/restore (CRIU) 过程中遇到的问题。CRIU 的开发者们在努力将一系列进程的完整状态记录到持久性存储设备(persistent storage)中,然后在未来的某个时刻,可能在另一台机器上重新启动这些进程。这个努力已经有了一些进展。不过这个功能的难度往小里说,也是非常有挑战性的,但 CRIU 的开发者们又给自己增加了一个额外难度:需要从用户空间(user space)来完成整个流程。多年来,人们一直在尝试实现 kernel-based checkpoint 机制,但没有哪个方案是走到快要合入 kernel 的程度。所以,在 user space 实现,似乎是解决 checkpoint/restore 任务的唯一可行方案了。
CRIU 可能会被移到到 user space 去,但内核社区仍然需要在一些地方增加支持功能,从而让其真正可行。例如,userfaultfd() 功能有助于进程内存的迁移,clone()系统调用的各种特性有助于重新创建进程的动作。这些辅助工具使得 checkpoint/restore 的工作真正可行了,同时还能将内核从这个过程中的大部分工作中解放出来。
CRIU 的开发者在早期遇到的一个问题是怎样确定两个已经打开的文件描述符(可能来自不同的进程)是否指向内核中的同一个已打开的文件。创建这样的文件描述符很容易,可以用 dup()或 clone()来完成;然后可以通过进程间通讯的 SCM_RIGHTS datagram 来传递给不相关的进程。对于 CRIU 来说,只要查看/proc 中的条目,就可以很容易地确定两个文件描述符引用的是同一个文件,而在还原时就可以在这两个地方按原样再打开该文件,就能恢复出 checkpoint 创建时的情况。
然而,如果两个文件描述符指向同一个打开的文件——换句话说,如果它们指向内核内的同一个 file struct——那么在还原时如果用两个完全不相干的描述符来替换的话就可能会破坏应用程序。CRIU 可以正确地还原这些描述符,但前提是要能在创建 checkpoint 时就检测出它们是相关的。这种检测是当时内核尚未支持的。
要查询文件描述符的出处,主要需要从内核的内部数据结构来获取这个信息,而要把这些信息提供出去就必须非常谨慎。早期讨论过的一个想法是让内核将每个文件描述符背后对应的 file struct 地址给 export 出来。如果两个描述符显示出相同的地址,那么它们就是指向同一个东西,重新创建时就要恢复这个样子。但是内核为了防护恶意攻击,会将其数据结构的地址隐藏起来。这种做法有时候很难做完美,但人们都认为这样做是正确的方向。所以大家不会赞同这个方案中的暴露地址的方法。
相反,开发者最终增加了一个系统调用来针对 “这两个描述符是一样的吗?” 这个问题给出直接回答,这就是 kcmp():
int kcmp(pid_t pid1, pid_t pid2, int type, unsigned long idx1,
unsigned long idx2);
如果 type
是 KCMP_FILE,那么内核将检查 ID 为 pid1
的进程中的文件描述符 idx1
是否与 pid2
进程中的描述符 idx2
相同。还有其他一些 resource 也可以用同样的方法来调用此 API,都是为了回答同一个问题:这两个 resource 是同一个东西吗?对于内核来说,回答这个问题的话比起提供 file struct 结构地址要安全得多,但是仍然有一些限制,特别是,调用 kcmp 的进程必须要有权限在两个目标进程上使用 ptrace(),而且这几个进程必须都在同一个 PID namespace 之内。
尽管施加了这些限制,可是还是有一些人对 kcmp() 感到不太放心。于是为了尽量避免有人利用这 kcmp() 来进行破坏,人们就将这个系统调用改为只有配置了 checkpoint/restore 功能的 kernel 才会打开。这样一来它在大多数内核上都完全不存在,也就不能被用来攻击这些内核了。
但在现实世界中,内核开发者对内核配置选项的选择并不能代表大多数的情况。大多数用户运行的内核都是由发行商构建的,而发行商有动力去启用尽可能多的功能,哪怕只有较少用户需要这些功能。大多数人不会对内核中不需要的代码提出抱怨,毕竟他们可能根本不知道这些代码在那里,但是如果他们需要的一些功能无法正常工作的话,他们肯定会抱怨。所以,虽然 checkpoint/restore 功能的用户比较少,但发行商(例如 Fedora 和 Ubuntu)还是因为有少数人需要它而将这个功能打开了。这使得 kcmp()也被广泛地开放了出来。
如果你提供了一个功能,那么就会有人来使用它,可能是按照你没有想到的的某种方式来使用。这个定律很准。Mesa 图形库就为 kcmp()找到了一个与 checkpoint 无关的用法。有时,该库会发现自己需要处理多个指向同一底层 DRM device 的文件描述符。在这种情况下,修改其中一个文件描述符的话会影响到另一个文件描述符,而且可能会导致出错。为了避免这个问题,Mesa 会在需要时使用 kcmp() 来进行检查,从而确保文件描述符没有跟其他人公用。
当然,只有当 kcmp() 在运行中的内核中确实存在时,这种检查才能正常工作。但是并不是所有的发行版都打开了这个选项。如果要求这些发行版为了 kcmp() 而打开 checkpoint/restore 功能的话,似乎有些矫枉过正,所以 Chris Wilson 提交了一个 patch,让 kcmp() 不再依赖 checkpoint/restore 功能就可以独立 enable。在描述这个补丁的必要性时,Daniel Vetter 说:
也许最开始是一个错误的做法,但我们的用户空间程序已经开始依赖 sys_kcomp 进行 fd 比较。所以无论是对是错,如果你想运行 mesa3d gl/vk stack 的话,你就需要这个 kcmp。也许这并不是最聪明的想法,但是因为有许多发行版默认启用了这个功能,所以之前没有发现这个问题,而现在我们已经将这个功能推广开来了。
最初实现这个功能的 Michel Dänzer 进行了辩护,认为使用 kcmp() 是个正确选择,并对它没有被缺省打开而感到惊讶。他也询问大家应该选择什么其他的解决方案,但没有得到回答。Kees Cook 指出 kcmp() "是一个非常强大的系统调用",但它的使用有限制,而且既然已经被广泛使用了,"所以也许可以公开这个功能了"。
patch 第一个版直接默认启用了 kcmp(),但哪怕这不会引入任何已知安全问题,这么做也违背了通常的原则。所以,到了第三版就默认改为 "no" 了。不过,如果启用了 checkpoint/restore 或 graphics,那么这个系统调用就 enable,这意味着在大多数内核上都缺省可用 kcmp() 了。不出意外的话这个 patch 会被合并到 5.12 中。如果没能赶上的话,发行商也可能会将它 backport 移植到旧版内核上。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~