LWN: uretprobe 新的实现引入的问题!

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

The trouble with the new uretprobes

By Jonathan Corbet
January 23, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1005662/

“uretprobe”(用户态返回探针)是由内核注入到正在运行进程中的动态用户空间跟踪点(dynamic, user-space tracepoint);这篇文档 简要描述了它们的使用方法。 其中,uretprobe 被 perf (性能分析工具)实用程序用于统计函数调用耗时。6.11 内核对 uretprobe 进行了重大更改,提高了它们的性能,但此更改也给某些用户带来了麻烦。解决问题的最佳方法尚不明确。

具体来说,uretprobe 的存在是为了获取目标进程中函数返回时的信息。旧版本的内核通过注入代码来实现 uretprobe,该代码在进入函数时会将返回地址更改为一个特殊的跳转指令(trampoline),该指令又包含一个断点陷阱指令。当目标进程执行该指令时,它将自陷(trap)返回到内核,内核随后提取所需信息(例如函数的返回值)并运行任何其他附加代码(可能是 BPF 程序),然后允许进程继续运行。此方法有效,但也对被探测进程的性能产生明显的影响。

为了提高 uretprobe 的性能,Jiri Olsa 提交了一组补丁集,该补丁集更改了 x86 系统上的实现方式。返回跳转指令(trampoline)仍然存在,但它不再触发陷阱,而是直接调用新的 uretprobe() 系统调用,然后处理所有相关工作。由于系统调用处理比陷阱处理更快,因此使用 uretprobe() 时,对被探测进程的开销更低。此新的系统调用不接受任何参数,并且只能从内核注入的特殊跳转指令(trampoline)中调用;否则,它只会向调用进程发送 SIGILL 信号。

可以说,所有系统调用都是特殊的,但这一个达到了一个新的水平。它不是进程可以简单调用以从内核获得有用服务的调用。因此, uretprobe() 不太可能出现在任何人的“每个程序员都应该知道的五个新系统调用”列表中。但是,它确实成功地将 uretprobe 的加速效果提高了约 30%。此更改已进入 6.11 版本,似乎没有产生不良影响。

然而,1月 10 日,Eyal Birger 报告了一个不良影响;实现 uretprobe() 的内核会导致 Docker 容器崩溃。问题在于,Docker 使用seccomp()(安全计算模式)来强制执行容器化系统可以调用的系统调用的策略。Docker 使用的策略是,正如标准做法所建议的那样,是一种缺省默认拒绝的方式;如果未明确启用给定的系统调用,则将被阻止。 uretprobe() 不在任何 Docker 开发人员的新型令人兴奋的系统调用列表中,因此未在允许列表中找到。结果,将 uretprobe 注入到在 Docker 下运行的进程中会导致该进程过早且神秘地死亡。Docker 用户实际上将不再注意到被跟踪进程的性能下降,但他们不太可能为此表示感谢。

当时提出了各种解决问题的可能性。Olsa 编写了一个快速补丁来检测执行 uretprobe() 失败的情况,并在这种情况下回退到旧的实现。他还考虑在使用 seccomp() 时完全禁用 uretprobe() 实现,或者添加一个 sysctl 旋钮来控制是否使用 uretprobe() 。但是,Birger 不喜欢sysctl的想法,他说:“‘给我提供更快的速度,但可能导致出现一些我无法控制的进程崩溃’是一种奇怪的选择”。

Oleg Nesterov 反而建议修补 seccomp() 以简单地忽略对 uretprobe() 的调用,使该系统调用更加特殊。Birger 随后提交了一个补丁来实现 Nesterov 的建议。然而,Kees Cook 对这一更改提出了质疑,想知道为什么 uretprobe() 需要如此特殊。他指出,Docker 已经处理了其他奇怪的系统调用,例如sigreturn()(信号返回);它应该能够对 uretprobe() 做同样的事情:

基本上,这是一个 Docker 问题,而不是内核问题。Seccomp 的行为是正确的。我不想在没有充分理由的情况下开始使系统调用不可见。

Birger 回应道,这种情况确实不同:

我认为不同之处在于,此系统调用不是进程代码的一部分——它是通过跟踪它的另一个进程插入的。因此,这与希望部署使用新的 libc 或新的系统调用的二进制文件的新版本不同。

然而,这种理由强化了Cook 的立场。他表示,进程可能希望通过使用 seccomp() 阻止 uretprobe() 的注入来进行防御。他补充说, seccomp() 的重点是实施赋予它的策略; seccomp() 本身不应该有单独的策略。

这种推理虽然合理,但却无助于解决 Birger 的问题,他表示,问题很简单:

我们面临的问题是现有工作负载正在中断,而且正如我提到的那样,我不确定由于性能原因添加的新系统调用而要求替换有效工作的 Docker 环境是否实用。

他表示,这种替换并非易事。他最后想知道,正确的解决方案可能是简单地把 uretprobe() 改动 revert 掉。Olsa 再次表示,最好引入一个新的 sysctl 旋钮来控制是否使用 uretprobe() ,但 Cook 回答说,至少目前来说,去掉这个改动可能是最佳选择,同时需要考虑这种实现应该如何与 seccomp() 等功能交互。然后,Olsa 建议,最好的解决方案可能是暂时禁用 uretprobe() ,而不要将其从内核中删除,直到 Docker 可以更新以正确处理它为止。Birger 开始考虑这个想法。

这种方法可能会为这个问题找到一个解决方案,尽管可能需要数年时间才够完成绝大多数的 Docker 安装环境,然后才能安全地重新启用 uretprobe() 。但我们将再次遇到这个问题。在一个拒绝所有未明确启用的系统调用的沙箱中运行系统可能对安全性有益,但当底层系统内核例行添加新的系统调用时,这种做法将遇到麻烦。除了 uretprobe() 之外,x86 架构在 2024 年还增加了九个新的系统调用: setxattrat() , getxattrat(), listxattrat(), removexattrat(), mseal(), map_shadow_stack(), lsm_get_self_attr(), lsm_set_self_attr() 和 =lsm_list_modules()=。没有理由相信系统调用的添加会就此停止。

某些方面必须让步;在这种情况下,似乎是新的 uretprobe 实现。但很难想象开发社区会对不能添加新的系统调用以免破坏现有的 Docker 实现感到高兴。也许不会再有像 uretprobe() 那样特殊的系统调用了,它能够仅通过内核更改就破坏系统,但是,正如 Cook 指出的那样,在添加更“正常”的系统调用导致类似崩溃的情况下也有先例。总而言之,“不允许任何新事物”和“添加许多新事物”的组合每隔一段时间就会爆炸,这并不令人意外。

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

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

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

38561aea24214aaf564a718340013243.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值