LWN:seccomp用户空间通知功能以及信号处理!

共 3100字,需浏览 7分钟

 ·

2021-05-01 21:07

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

Seccomp user-space notification and signals

By Jonathan Corbet
April 9, 2021
DeepL assisted translation
https://lwn.net/Articles/851813/

seccomp()机制允许增加一个 filter program 过滤程序(以 "classic "BPF 的形式),来对是否允许进程调用某个系统调用做出决定。user-space notification 功能可以把决定权进一步交给另一个进程来做。正如 Sargun Dhillon 最近的 patch set 所示,用户空间通知仍然有一些没有处理完美的场景,特别是在涉及到 signal 之时。这个 patch 进行了一个简单改动,希望能解决一个相当复杂的问题。这个问题是 Go 语言的抢占模式(preemption model)的改动引出的。

通常可以使用 seccomp() 来作为一种简单的缩减攻击面的方式,从而使得大部分系统调用对于目标进程来说都可以不受限制地使用。用户空间通知这个功能也可以用在这种情况下,但它的主要目的其实是另一种情况:允许那个监控进程来模拟完成目标进程的系统调用。举例来说,某个容器管理器(container manager)希望在容器内提供 mount() 功能,但需要严格限制可以 mount 哪些东西。这时就可以利用用户空间通知功能,来允许(拥有特权的,也就是 privileged)监控进程来真正进行这个它同意进行的 mount 动作,并将结果返回给目标进程。

当监控进程在处理一个拦截下来的系统调用时,目标进程会被阻塞在内核中,一直等待返回结果。不过如果该进程收到一个 signal(信号),那么就会终止这个等待状态,立即对该 signal 做出响应。如果该信号本身不是一个 fatal signal,那么可能会导致系统调用返回一个 EINTR 错误给此进程。并且,监控进程并不知道这个 signal,于是它后面还是会试图给内核提供对原来那个系统调用的处理结果。此时,它会得到一个 ENOENT 错误,表明之前等待的进程已经不存在了。

这会带来一些不方便之处,尤其是当监控进程已经替目标进程执行了某些耗费很多时间的任务的情况下。如果该 signal 没有杀死目标进程,那么很可能不久之后还会重试,导致一些额外的工作。不过大多数时候,在 seccomp() 监控之下运行的程序中很少会有这类 non-fatal signal。

Go signal

准确地说,这是过去的事实情况,但 Go 语言的开发者们也有责任。Go 语言的 "goroutine" 轻量级线程模型(lightweight thread model)要求 Go runtime(运行时)来处理 schedule 工作,也就是根据需要在 goroutine 之间切换确保它们都有机会运行。除此之外,偶尔还会出现 "stop the world" 的情况,也就是所有的 goroutine 都被暂停,从而让垃圾收集器(garbage collector)完成工作。之前实现这个功能的具体方式是让编译器在每个函数的开头来增加一个 preemption check 来处理。

但是,如果一个 goroutine 运行了很长时间却一直没有调用任何函数,那么会发生什么?如果该 routine 在某个非常严格的循环中执行的话就可能会发生这种情况。最坏情况下,比如这个 goroutine 可能一直在 spin 等待一个锁,导致锁的持有者(另一个 goroutine)都没有机会运行,也就无法释放这个锁,这种情况往往会导致用户不满。另一种也会导致 goroutine 之间的 preemption 的情况,就是执行那些耗时很长的系统调用。

Go 开发者们已经尝试了一些方法来解决这个问题。其中一种就是在代码向回跳转(backward jump)的地方插入 preemption check(例如在某个循环的末尾)。但是哪怕将检查代码减少到只有一条指令,这带来的性能损失也还是太高了。这种方法对于那些耗时很长的系统调用场景也没有帮助。所以 Go 社区决定用一种非合作性的抢占机制(non-cooperative preemption mechanism)来解决这个问题。简单来说,任何一个运行了 10ms 而没有 yield(让出)过的 goroutine 都会收到 runtime 发出的 SIGURG signal,然后 Go runtime 运行时会重新安排一个线程来执行、启动垃圾回收、或者做其他什么需要在此时做的工作。

而通过 seccomp()转给另一个进程执行的系统调用往往会比平时运行的时间更长长,而监控进程可能在执行的相应任务(例如 mount 文件系统)可能需要更加长的时间。显然,这导致了 Go 程序中大量的以 seccomp()转发出去的系统调用被打断,人们当然希望找到一种方法来避免这些中断事件。

Masking non-fatal signals

为了解决这个问题,Dhillon 的 patch set 为 SECCOMP_IOCTL_NOTIF_RECV(监控进程接受通知会用这个)ioctl() 增加了一个新的 flag(SECCOMP_USER_NOTIF_FLAG_WAIT_KILLABLE)。如果在向监督进程发出通知时设置了这个 flag,目标进程就会进入 "killable" wait 状态,这意味着 fatal signal 仍可以被正常传递处理,但任何其他 signal 都会被屏蔽掉,直到监控进程处理完成之后再说。因此,non-fatal signal 将不会再打断系统调用(此时监控进程正在处理这些系统调用)。

请注意,如果在监控进程收到通知之前,就出现了一个 non-fatal signal 到达,那么目标进程的这个系统调用仍将像从前一样被打断。此时这个 seccomp 通知将会被取消掉,如果监控进程试图去读取该通知的话,会收到一个 error。在这种情况下,最终结果就像系统调用一开始就没有发生过一样。不过,一旦通知被送达,系统调用就会得到执行,并一直运行到结束。这个改动相对较小,它解决了这个问题,尽管这个解决方案是在使用 seccomp()和用户空间通知功能时可能会给 Go 的抢占机制增加不确定时长的 delay 为代价的。delay 其实本来也正是抢占机制想要预防的,但这个 delay 至少是在监控进程的控制之下,而且应该不会是无限延迟。

截至目前,这组 patch set 已经发布了两次,它没有收到多少回应。这可能表明,到目前为止很少有人看过它,这对于用户空间 API 的安全相关的改动来说,不是一个好兆头。在得到更多 review 之前,这项工作不太可能取得进展,使用 seccomp()和用户空间通知功能的 Go 用户也将继续忍受当前的困扰。

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

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

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



浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报