LWN:seccomp()里永远允许的系统调用bitmap!

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

Constant-action bitmaps for seccomp()

By Jonathan Corbet
October 22, 2020
DeepL assisted translation
https://lwn.net/Articles/834785/

seccomp()系统调用,可以让用户空间加载一个或多个 BPF program(这里指 classic BPF program,不是 eBPF),此进程在调用系统调用时就会运行这些 BPF program,可以用来在某些情况下检查每个系统调用的参数,来告知内核是否应该允许此系统调用继续进行。这个功能主要用在一些容器化解决方案中(当然也有其他场景),作为减少内核受攻击面的一种防护手段。不过,在某些情况下,使用 seccomp()可能会导致性能出现大幅下降。目前,为了减少 seccomp()在一个常见应用场景下的性能损失,人们提出了两种方案。

seccomp()的参数检测功能(argument-inspection)在很多环境下都很有用处,例如,它可以阻止对所有文件描述符(除了标准输出以外)的 write()调用。但是,现实世界中,许多场景都没有利用这个能力,相反地,这些场景中只会根据调用的是哪个系统调用来做决定,而不注意这个系统调用的参数。事实证明,对于这种情况来说,BPF 机制没有进行针对性的优化,实现方式是简单地跟系统调用号逐项进行比较。这些比较操作的开销完全可以通过使用更好的算法(例如先检查最常用的系统调用)来减少,但无论如何这种优化是有性能上限的,每个系统调用都会因为这个检查而变慢。

许多比较工作都是浪费。如果在 seccomp()配置中,允许了一次 read()操作,那么后续每次这种操作都会被允许,但每一次 kernel 都在硬着头皮再做一次比较。如果有某种方法可以确定地知道某个 seccomp() filter program 会允许或拒绝这种系统调用,完全不看调用参数,那么就可以更快地决策。

Optimizing seccomp()

6 月份的时候 Kees Cook 发布了一个实现这种优化的 patch。方案是在一个进程中创建三个 bitmap(分别称为 allow、kill_thread 和 kill_process),这些 bitmap 都是以系统调用号为索引。当一个系统调用被 seccomp()看到时,就用系统调用编号在这些 bitmap 中来查询。只要相关的 bit 在 bitmap 中置位了,就允许这次操作,不再需要去运行 BPF program。因此,我们可以在 allow bitmap 里面可以把那些总是会被允许的系统调用置位,这样它们就能更快地执行了。

这种方案的诀窍在于如何确定该对哪些 bit 置位。Cook 的 patch set 里,在加载 BPF program 时就针对每个系统调用都实际执行一次这个 BPF program,观察会有什么后果。如果对于某个系统调用,BPF 代码并没有访问系统调用的参数,内核就可以断定这个系统调用的检查结果每次总是相同的,就可以在相应的 bitmap 中置位了。反之,如果有访问过这些参数,那么就会在所有 bitmap 中清除对应的 bit,从而,后续每次调用该调用时都会执行 BPF program。

这里还有一个麻烦:如何确认 BPF program 是否真的访问了系统调用的参数。patch set 第一版 e 的时候是将参数放在一个单独的 page 中,然后运行 BPF 代码,查看 page-table entry(页表项)来确认该页面是否被用过。这个机制,可行,但依赖复杂的内存管理技巧。

Jann Horn 有一个更好的想法:简单地模拟执行 BPF program 执行,然后直接观察它做了什么。关键一点,这里模拟执行的功能不需要很强大,因为比较系统调用号的 program 往往很简单。只需要模拟出一小部分指令就好,如果模拟中碰到了不能识别的指令,就可以判定是设计复杂逻辑,无法走 bitmap 优化的路径。

9 月 21 日,YiFei Zh 提出了一组 patch,用模拟执行方法(emulator)来实现了一个固定动作的 seccomp() bitmap,可以 l 用来确定系统调用的参数是否被 BPF program 用到了。这与 Cook 的方法还有一些其他的变化,例如,只有 "allow" bitmap,因为这些"deny"场景其实并不需要优化。两天后,Cook 发布了一个新版本,其中使用了非常简单的 emulator,更接近 Horn 最初提出的设计。此后不到一天,Zhu 也又提出了一个修改版本,其中简化的 emulator 借鉴了 Cook 版本的一些想法。

库克认为 Zhu 最初的 patch set "明显过度设计了",他新提出的更新版本是赶时间写的,用来展示 "我希望 emalator 能有多小"以及如何改进架构支持。从那时之后,Cook 实现中的许多想法都被 Zhu 吸取了。10 月 11 日 Zhu 发布的第 5 版 patch set,在 kernel/seccomp.c 中增加了 292 行代码(初始版本为 400 行),并且支持了更多的功能。Cook 此后再没有再提出过新的改动,说明 Zhu 的版本可能是最终会合并的版本。

Paths not taken

这里有一个有趣的问题需要考虑。模拟 BPF 的执行并观察发生了什么,这似乎并不是一个最优雅的解决方案,至少有两种其他的方法。

  • seccomp() program 的开发者肯定知道他们想要怎样。可以创建一个新的 seccomp()API,允许用户空间直接传入 bitmap,而不需要在内核中进行逆向工程。

  • seccomp()是内核中为数不多的仍在使用 classic BPF 的地方之一。可以换成 extended BPF,就可以让写好的 program 能更快地做出决定,这样一来,同样不需要在内核中添加代码来猜测这个 program 是做什么。

在讨论这些 patch set 的时候,这些方法的讨论相对较少。Horn 确实提到过,创建一个新的 API 需要改变用户空间程序才能用起来,而目前的 patch set 只会让现有的 program 运行得更快,不需要用户空间做任何改动。这组 patch 也没有改动 seccomp()的用户空间 API,这意味着内核社区不用增加支持新的 API。相反,如果新增一个加载 bitmap 的 API 的话,内核将不得不永远支持这个 API。

关于 eBPF,之前也有几次试图为 seccomp()添加 eBPF 支持。最后一次似乎是 Sargun Dhillon 在 2018 年发布的一组 patch。不过要想进入 mainline 的话,还需要解决一些问题。BPF 维护者担心在 seccomp()中使用 eBPF 会限制 eBPF 本身的未来发展。而面向安全的开发者则担心 eBPF 提供的额外能力以及扩展的受攻击面。把 seccomp()和 eBPF 放在一起,很可会引入新的漏洞。还有一个小问题,seccomp() filter 可能是无特权的进程加载的,而 eBPF 这边想支持让无权限的代码加载的话大家一直表示反对。

最终,目前的 patch set 似乎是在短期内提供改善 seccomp()性能的最好办法。根据 Zhu 的补丁集包含的一些性能指标,使用 bitmap 确实会带来性能提升。所以预计这个优化会被合并。大概是在 5.11 开发周期中吧。

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值