LWN:跟set_fs()说再见吧!

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

Saying goodbye to set_fs()

By Jonathan Corbet
September 24, 2020
https://lwn.net/Articles/832121/
DeepL assisted translation

set_fs()函数在 Linux 内核的最初阶段就已经存在了。它是分离用户空间和内核空间的内存的机制中的关键部分。它很容易被滥用,并且多年来一直跟各种安全问题牵扯不清,因此内核开发者一直希望去除 set_fs()。他们没能在 5.10 内核中完全实现这个目标,但是,经过几个月来悄悄进行的工作,set_fs()的终结时刻很快就要到来了。

2017 年的这篇文章(https://lwn.net/Articles/722267/ )详细描述了 set_fs()及其历史。简而言之,set_fs()对地址空间的用户空间部分和内核部分进行了边界设置。在某个进程触发 set_fs() 调用设置了边界之后,低于这个边界的虚拟地址都可以由该进程访问,不过在页表中设置的内存访问权限仍然需要遵守。任何超过这个边界的地址都属于 kernel 的。

通常情况下,这个边界应该是个固定值。当人们需要更改这个边界的时候,通常都是出于同一个原因:某些内核子系统需要调用一个访问用户空间数据的函数,但是当时是在内核空间地址。例如,如果我们要做一个把文件内容读到内存 buffer 的简单任务,就像 read()系统调用所做的,但它会执行所有通用的访问检查,这意味着它会拒绝把数据读入并搬移到内核空间的 buffer 里。如果一个内核子系统必须要进行这样的读取操作,那么它首先调用 set_fs()来禁用这些检查,只要一切顺利,在工作完成后,就会用另一个 set_fs()调用来恢复旧的边界。

当然,历史已经证明,不可能总是一切顺利的。因此,很自然地,开发社区多年来一直希望摆脱 set_fs()。同样,很自然地,这个工作到现在都没有完成。内核项目并不缺少开发者,但总是缺少愿意和能够做这种深层次基础工作的人,它往往在各个公司的市场计划中都不受重视。所以去掉 set_fs()的任务已经搁置了很多年。

不过最近,Christoph Hellwig 已经开始着手这项任务,同时也在进行相关的内核代码清理工作。

例如,人们可能会惊讶于在核心的网络代码中也有 set_fs()调用,更出乎意料的是,它们是在 2019 年 5.3 开发周期中添加的。相关补丁允许 BPF 程序调用 setsockopt()和 getsockopt()这两组系统调用。而这些系统调用通常是供用户空间调用的,因此它们会对传递给它们的任何参数进行通用的访问检查。不过,来自 BPF 程序的调用提供的都是内核空间内的 buffer。在这种情况下,加入 set_fs()调用,就可以让这些系统调用不需要修改就能直接工作。

Hellwig 的计划是取消这个对 set_fs()的调用,这需要创建一个新的 sockptr_t 类型,它可以存放指向内核或用户空间的地址。

typedef struct {
    union {
        void  *kernel;
        void __user *user;
        };
        bool    is_kernel : 1;
} sockptr_t;

初始化 sockptr_t 变量的代码必须明确指定该地址是内核空间还是用户空间。然后可以使用一组 helper function 将数据复制到该地址或从该地址复制数据,而不需要担心目标 buffer 位于哪个空间,也不需要调用 set_fs()。后来发现,setsockopt()和 getsockopt()提供了很多不同的选项参数,所以需要许多 patch 来完成这个将相关函数转换为 sockptr_t 地址的工作。在这组 patch 的最后,网络系统中的 set_fs()调用就被删除了。这个系列在 5.9 合并窗口期间进入了主线。

不过,本来是希望能实现一个在整个 kernel 里面通用的去除 set_fs()的方案(https://lwn.net/ml/linux-kernel/20200624162901.1814136-1-hch@lst.de/ ),这个方案没能合入 mainline。Hellwig 提议创建一个 "universal pointer "类型(uptr),它的功能就像 sockptr_t 一样,还附带了一对新的 file_operations 方法专用于配合这些指针一起使用。然后,任何可能需要对内核空间和用户空间指针进行 I/O 的内核子系统,都可以被改为使用这些新方法,而不是调用 set_fs()。

Linus Torvalds 否决了这个想法。他反对增加新的类型和 file_operations 方法,他认为这些方法是针对一个真实问题来实现的临时 workaround,没有必要。他问道,如果有人要费心费力地将普通 read()和 write()调用转换为新的 read_uptr()和 write_uptr(),为什么不直接转换为现有的 read_iter()和 write_iter()方法呢?这些方法已经能很好地处理不同的地址空间了(是通过 struct iov_iter 中的另一个 union 来跟踪正在使用的地址类型)。确实,在内核中许多地方删除 set_fs()调用的时候都改成了使用 iov_iter。所以,uptr 类型就被抛弃掉了,但 sockptr_t 克服了 Torvalds 的反对意见,最终合入了 mainline。

接下来,还有一些隐式的 set_fs()调用需要处理。在当前内核中,内核和用户空间的边界是在启动过程中相当晚的时候(不过还是在 init 过程启动之前)才建立起来的。在这个时间点之前,在用户空间指针上进行操作的内核函数通常会很乐意直接使用内核空间指针。有一部分初始化代码(例如处理 ramdisk 初始化的代码)很依赖这种行为。为了消除隐式的 set_fs() 调用,需要另一个补丁系列来创建一组特殊的 helper function,一旦启动过程完成了,这些 helper function 就会被丢弃。这组 patch 也合并到 5.9 版本中了。

最后一步(至少对于 x86 和 PowerPC 架构来说是最后一步),是这组 patch(https://lwn.net/ml/linux-kernel/20200903142242.925828-1-hch@lst.de/ )彻底删除了 set_fs() 。要达到这个目的,需要做一些细节清理。例如,它为 proc 文件系统增加了 iov_iter 支持。还有个 patch(https://lwn.net/ml/linux-kernel/20200903142242.925828-6-hch@lst.de )将 kernel_read() 和 kernel_write() (这是另一种在内核空间 buffer 上执行 I/O 的方式) 转换为 iov_iter, 删除了之前在那里使用的 set_fs() 调用。splice()系统调用也改过了,不过可能会破坏现有用到它的地方的兼容性:如果数据来源是不支持 splice_read()方法的设备,它就无法正常工作。Hellwig 表示,受影响的用户似乎都可以在这种情况下回退到别的工作机制上,但如果后续发现有需要的话,也可以给相关的设备添加获得 splice_read()实现。

再打几个 patch 把 set_fs()最后的调用都从 x86 和 PowerPC 架构中删除后,set_fs()本身就不再支持了,任务也就完成了。这些 patch 目前在 linux-next 中,因此应该会被包含在 5.10 版本中。Hellwig 也发布了一个针对 RISC-V 的 patch set,Arnd Bergman 有一个针对 Arm 的 patch set,但这些 patch 都还没有被合入。Hellwig 打算在剩下的架构上再做些工作,把 set_fs()从每个架构中都删除。

上面描述的这些 patch 都仅仅是为最终摆脱 set_fs()所做工作的一小部分。所有这些工作的最终结果是差不多可以消除一个内核接口了,而且这个接口几乎在它存在以来就一直被认为是有风险的,并且它已经存在了很长时间。这正是一个内核开发过程的很好的例子,这些工作往往不会出现在头条新闻上,但却能静静地让内核保持长期的可维护性。像这样的任务通常会缺乏关注,但从长远来看,它们总还是会被完成的,这是件好事。即使是在近 30 年后,内核中仍有很多清理工作要做。

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值