LWN: 越来越弱化的 ETXTBSY!

Linux News搬运工

共 4389字,需浏览 9分钟

 ·

2021-09-12 11:41

关注了就能看到更多这么棒的文章哦~

The shrinking role of ETXTBSY

By Jonathan Corbet
August 19, 2021
DeepL assisted translation
https://lwn.net/Articles/866493/

类 Unix 系统有许多方法可以很容易地就让新用户感到困惑,其中不少都是在 Linux 加入进来之前就已经存在的了。一直以来就有一个困惑是关于 "文本文件繁忙"(ETXTBSY,text file is busy)这个错误信息,这是在试图覆盖一个可执行文件时会出现的错误。Linux 跟前辈们不太一样,已经不太可能出现 ETXTBSY 的错误值了,但确实仍然偶尔会发生。最近为简化 ETXTBSY 底层机制所做的工作就提出了一个更基本的问题:这种错误检查到底有没有价值?

在这种情况下,所谓的"text" 正在被使用(busy),实际上指的是程序的可执行代码正在被使用。也就是指由 CPU 读取的 text,而不是由人类读取的那些普通文本。当程序运行时,它的可执行 text 代码段内容被映射到了这个运行进程的地址空间之内。此时,Unix 系统传统上会开始阻止对这个文件进行修改。或者就干脆允许随意修改这个在运行的程序文件,不过一般来说都不会有好结果。此外,由于更改后的代码只有在被加载到 RAM 时才会被读取,这意味着很可能要在文件被覆盖的几个小时(或几天)后才会出现问题。因此,为了避免反复向用户解释为什么他们的程序会莫名其妙地就 crash 了,Unix 内核开发者在很多年前就决定了在程序运行时冻结它的相应文件,于是就变成需要向人们解释 ETXTBSY 这个错误了。

产生这种错误的最简单的方法也许就是在某个进程仍在运行时试图编译生成这个程序文件。开发人员(指那些使用需要编译的语言开发程序的人)往往很早就学会了处理这种 "text file busy" 的错误,也就是直接 kill 掉当前正在调试的程序,然后再重新运行 make 命令。

How it works

在内核内部使用了 inode 结构来表示文件。这个结构中有一个字段,是个类型为 atomic_t 的名为 i_writecount 的变量。一般来说可以把这个字段当作文件被用 write 方式打开的次数计数。然而,如果 i_writecount 小于 0,那么就会被当作该文件的写入被阻止过多少次。如果该文件是一个可执行文件,运行该文件的每个进程都会在运行期间对 i_writecount 进行递减操作。也就是说,这个字段的功能实际上是一种简单的锁保护机制。只要值是负的,文件就不能被采用可写入的方式来 open;相反,如果值是正数,那么就不会阻止写入操作(write access)。类似的情况还有试图执行一个当前已被可写入方式 open 的文件,也会触发 ETXTBSY 而失败。

在当前内核中,可以通过调用 deny_write_access()来阻止写访问,但更常见的方法是利用 VM_DENYWRITE 标志来创建 memory mapping。例如,execve() 系统调用会把可执行文件的 text 代码段内容 map 到设置了 VM_DENYWRITE 的内存区域中,这个 mapping 就会导致 i_writecount 被递减(当然,如果该文件本来就是 open 的那就无法设置成功了)。当 mapping 消失时(例如正在运行的程序退出了或调用了 execve()),那么 i_writecount 将再次被递增。等它达到零的时候文件将再次变成可写文件。

早期 Linux 阶段,比 Git 时代更早的时候,mmap()系统调用支持一个名为 MAP_DENYWRITE 的 flag,这会在内核中导致 VM_DENYWRITE 被置上,从而在 mapping 存活期间阻止对这个 map 进来的文件的可写方式访问。但这个选项还有一个问题:任何一个可以打开文件来进行读取的进程都可以利用 MAP_DENYWRITE 来创建 mapping,从而阻止系统中其他进程对该文件进行写入。这最起码也是一种很容易被利用来进行拒绝服务攻击(denial-of-service)的一种方式,所以很早就被删除了。调用 mmap() 的时候如果设置了这个 flag 的话仍然会成功,但该 flag 会被直接忽略掉。

Shared libraries

当时移除 MAP_DENYWRITE 带来了一个有趣的副作用,其实都不太明显。人们可能认为一个文件,如/usr/bin/cat,才算是可执行的程序代码。但事实上,当人们运行 cat 时,大部分被执行的代码并不在该文件中,而是在许多共享库之中。这些文件内包含的可执行代码,因此就像普通所说的可执行文件一样,人们自然会认为它们在被使用时也会受到保护,所以不可能被写入。

曾几何时,情况确实如此,古老的 uselib()系统调用在 map 这些共享库的时候就禁止了 write 操作。然而,现在很可能没有系统仍在使用 uselib()了。在当前系统中,共享库主要是通过 mmap() 从用户空间映射进来的。MAP_DENYWRITE flag 就是针对这种使用情况而创建的,于是共享库在被使用时就不能被写入了。于是当 MAP_DENYWRITE 消失后,这个保护机制也随之消失。当前 Linux 系统中,有相应权限的用户完全可以覆盖写入正在使用的共享库文件。

这段历史的最终导致内存管理子系统中大堆的遗留代码(用来支持 MAP_DENYWRITE 和 VM_DENYWRITE)不再有任何实际用途。所以 David Hildenbrand 决定把这些代码移除。在合入他的这组 patch set 之后,execve() 将直接调用 deny_write_access(),而 mmap() 根本就不用考虑这种情况。这就导致了一个用户空间可见的 API 的变化:uselib()不再阻止对共享库的写入访问了。估计不会有任何人会受到这个差异的影响。

An idea whose time has passed?

在对 Hildenbrand 的 patch set 进行 review 时,GNU C 库开发者 Florian Weimer 指出,GNU C 库有 "一个持续存在很久的问题,即人们会使用 cp(或类似的工具)来替换这些系统库文件"。他没有提到 C 库的开发者事实上很久以前就不再有耐心向这些用户解释为什么他们的应用程序为什么会莫名其妙的 crash 了,确实也没有必要提这一点。他说,如果能提供一种方法来阻止这种错误,或者能明确地告诉人们 crash 是由库文件被覆盖引起的,那就更好了。他认为有许多方法可以避免使用以前的 MAP_DENYWRITE 就能达到目的:

讨论就此转向了有哪些其他方法来保护共享库在使用过程中不被覆盖。例如,Eric Biederman 建议在安装 C 库文件的时候就设置 immutable bit。但是 Linus Torvalds 明确表示他认为问题出在其他地方:

内核 ETXTBUSY 功能纯粹是我们出于 courtesy 而额外提供的功能,而且人们已经注意到由于各种原因导致它只对可执行文件主程序有效。用户空间甚至就不应该依赖这个功能,它更像是一个 "好吧,你正在做一些令人难以置信的愚蠢的事情,当我们注意到时,我们会帮助你避免自寻烦恼" 这样的功能。

在 Torvalds 重复了几次这一观点之后,Andy Lutomirski 建议彻底取消这个阻止 write 的机制:

往好里说,这个功能算是个奇怪的功能。它只适用于静态二进制文件(static binaries),而且它从未在我关心的问题中帮助过我。如果我正在重新编译的程序 crash 了,我并不关心。它甚至可能早就被一个不相干的 fatal signal 弄死了。实际发生的情况是,当我看到-ETXTBUSY 时会想 "等等,这不是 Windows,为什么会有 file sharing rules",然后我会想到 "等等,Linux 有一个半成品的 file sharing rule",然后就不再理会它了。

Torvalds 对这个想法表示赞同,尽管他担心某个角落里也许有一些应用程序可能会依赖于 ETXTBSY 的行为。但他指出,随着时间的推移,这个功能已经被逐渐削弱,而且到目前为止还没有人抱怨过。可以尝试一下移除这个功能,他继续说道:"最坏的情况是,我们不得不再把它加回去,但至少我们会知道有什么疯狂的东西还需要这个功能"。

不过 Al Viro 担心有一些安装脚本可能会依赖这个行为。Christian Brauner 补充说,允许对这些正在被使用的可执行文件进行写入可能会导致一些安全漏洞变得更容易出现。Hildenbrand 说,他的 patch set 已经使得这个阻止 write 的行为变得更加简单了,他赞成暂时保留这个功能。8月 16 日发布的第二版 patch set 中继续保留了可执行文件主程序的 ETXTBSY 行为。

Hildenbrand 的这个代码简化工作基本上肯定会在 5.15 合并窗口中被合入了。ETXTBSY 是否会完全消失,则不那么确定。摆脱这个功能对一些开发者来说会让代码变得更干净,但目前没有任何动力来推动移除这个功能。同时,采用以这种方式来改变系统行为的话,还是有可能会导致用户空间出现 regression 的。因此,更安全的做法还是暂时保留 ETXTBSY 这个功能。

[后记。Lutomirski 指向了强制锁(mandatory lock),认为这是内核中另一个实现了不受人们欢迎的文件共享规则的地方。这个功能确实不受欢迎。关于强制锁的内核文档最开始就有一小节在说明为什么人们不应该使用这个功能。2015 年的时候增加了一个 config 选项使得强制锁成为了可选项,一些发行版中已经合理地禁用了它们。ETXTBSY 讨论的一个潜在结果看起来可能是努力让其他发行版也做同样的事情,直到明确强制锁功能可以被安全地移除。敬请关注。]

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报