LWN:SA_IMMUTABLE以及修改signal处理行为有多危险!
共 3360字,需浏览 7分钟
·
2022-01-13 00:09
关注了就能看到更多这么棒的文章哦~
SA_IMMUTABLE and the hazards of messing with signals
By Jonathan Corbet
December 17, 2021
DeepL assisted translation
https://lwn.net/Articles/878768/
内核中有一些部分,是哪怕那些最有经验和能力的开发者也不敢涉足的。其中肯定包括 signal (信号)的实现代码。signal API 的性质就几乎决定了无论哪个 OS 里面的相关实现细节都会充满微妙的交互性以及复杂性,而 Linux 中实现的版本也同样是这个情况。因此,在 5.16 合并窗口的后期加入的一个 signal-handling 的改动一般人都会猜测会有潜在的问题的,最终果然如此。
Forced signals
signal API 通常允许任意进程来控制在自己收到 signal 时发生的事情。这包括捕捉信号、暂时屏蔽(masking)它或直接阻止(blocking)它。当然,也有一些例外,比如 SIGKILL 就不能被阻止或捕捉。在内核中,有一个更微妙的例外,那就是一个进程可以强制接受某个信号并退出,无论该进程本来可能打算如何处理。使用场景大多是为了应对那些无法恢复的硬件问题,但是 seccomp() 机制所发送的信号也是以这种方式来强制的。有些时候,信号是非常重要的根本不可以被忽视。
内核有一个函数用来通过这种方式强制发送信号,叫做 force_sig_info_to_task()。在需要的时候,可以用它来解除对目标信号(intended signal)的阻止(block),并将信号传递给目标进程;它还可以移除进程对该信号的处理程序(handler),将其重置为缺省行为(一般来说针对所用到的这些信号结果都是会 kill 进程)。但有趣的是,这个功能并不是总在 forced-signal 场景下来使用的,内核反而会直接 kill 掉目标进程,并设置其 exit status,使其看起来好像是 signal 导致的退出那样。10 月,Eric Biederman 发出了一组 patch,终于让内核做了它原本假装在做的事情,也就是真正地把信号传递过去,而不是伪装成这个样子。
这个改动达到了效果——除了一个由 Andy Lutomirski 指出的小问题之外。似乎在目前的内核中,目标进程中对 sigaction() 的调用有机会在 force_sig_info_to_task() 解除对信号的 block 状态以及实际交接信号之间这个短暂时间窗口中重新把这个信号 block 掉。对 ptrace() 的调用也会以这种方式跟 forced signal 产生竞态冲突。这种竞态冲突经常会导致错误行为,而且在某些情况下(比如从 seccomp() 来发送的信号)可能就上升成一个安全问题了。
Biederman 不想引入潜在的漏洞,他开始着手处理这种竞态冲突。解决方案就是使用一个新的标志(SA_IMMUTABLE),用在进程的内部信号处理信息上。这个标志通常是不需要设置的。如果这个标志发生了改变,那么随后任何试图改变有关信号处理行为的动作都会返回 EINVAL 错误来报错。这个标志在用户空间是看不到的,只能由内核来设置,而这种行为只有在 force_sig_info_to_task() 情况下才会发生。一旦 SA_IMMUTABLE 被设置了,就没有办法清除掉。也就是假设这个进程无论如何都必须要 exit()。这个改动解决了这个问题,而且由于这个标志对用户空间是不可见的,所以没有用户空间 ABI 的问题。或者说,我们之前是这么认为的。
这个 patch 于 10 月 29 日发布,并于 11 月 10 日进入 mainline(此时已经接近 5.16 合并窗口的尾声),作为 "exit cleanups" 工作的一部分合入了。等 5.16-rc1 内核正式发布了,这个 patch 就被那些 stable kernel 所接受,并在 11 月 18 日出现在 5.15.3 中。
Debugger bugs
不幸的是,在 5.15.3 发布的前一天,Kyle Huey 报告说,SA_IMMUTABLE 这个改动破坏了调试器(debugger)的行为。交互式的调试器中,经常需要代表被调试的进程来捕获信号,其中就包括一些被内核 force 发送的信号(并不是所有这种 forced signal 都是需要杀死进程的)。有了这个改动之后,trace()不再能够改变对 SA_IMMUTABLE 信号的处理了,事实上,根本不再传递这些信号了。Huey 说,这个 patch 应该被 revert 掉。然而,第二天它还是出现在 5.15.3 中了。
经过讨论,大家认为 SA_IMMUTABLE 的修改确实过于宽泛了,它 block 了一些以前可以正常进行的合法的信号操作。Biederman 在 18 日发布了两个 patch 来解决这些问题。第一个 patch 反映了这样的结论:不是所有被内核 force 的信号都应该设置 SA_IMMUTABLE 标志的,相反,应该仅限在内核打算让目标进程 exit 的情况下。这个意图是通过 force_sig_info_to_task() 的一个新增参数来实现的,所以在 seccomp() 子系统中对此函数的调用也都改过来了,从而实现了原定的目标。第二个 patch 增加了一个新的函数(force_exit_sig()),用在其他想要 forced exit 的地方,并增加了一些使用这个函数的调用者。
值得注意的是,在 forced exit 的情况下,trace() 在这些改动之后仍然无法捕获到信号。但比起所有这些 patch 合入之前,这个工作方式并没有什么改变。之前的实现(请记住是完全绕过了信号传递机制,所以)从来没有任何机会让 debugger 来处理。从用户空间看到的内核行为跟以前一样,没有发生改变。
这些 patch 似乎已经解决了这个问题。它们被合并到 5.16-rc2 中,然后在 11 月 25 日发现它们进入了 5.15.5。换句话说,原来那些 patch 导致的 regression,在 5.15-stable kernel 中存在了整整一周的时间。规则规定,在 mainline-rc 版本中出现之前,任何 patch 都不应该进入 stable kernel 内核。这次事件中,这个规则得到了遵守。5.16-rc1 在 patch 出现在 mainline 以及它出现在 stable update 之间。同样的规则可能推迟了将 fix 代码纳入 5.15 stable 发布中的时间,因为要到 11 月 21 日的 5.16-rc2 发布后,才能进行这个操作。
这里相关的问题是:我们采用的 one-release 的规则是否足以阻止在 stable kernel 中出现这种 regression?这条规则是为了应对以前出现的问题而增加的,而且事实上也确实可能阻止了一些 bug 被 backport 回来,但是显然有些 bug 还是经过了这个机制被 backport 到了 stable kernel。有一种观点认为,对于那些涉及到像 signal 这样的棘手子系统的 patch,应该需要采用更谨慎的方法进行 backport。不过,在缺乏开发者资源来做这些决定的情况下,目前的政策不太可能改变,稳定内核中后续应该还是会偶尔出现 regression(希望是短暂的)。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~