[英]Why does fork() fail on MacOs Big Sur if the executable that runs it is deleted?
如果删除正在运行的进程的可执行文件,我注意到fork
失败,子进程永远不会执行。
例如,考虑下面的代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
sleep(5);
pid_t forkResult;
forkResult = fork();
printf("after fork %d \n", forkResult);
return 0;
}
如果我在调用fork
之前编译它并删除生成的可执行文件,我永远不会看到fork
返回 0 的 pid,这意味着子进程永远不会启动。 我只有一台运行 Big Sur 的 Mac,所以不确定这是否适用于其他操作系统。
有谁知道为什么会这样? 我的理解是一个可执行文件应该可以正常工作,即使它在运行时被删除。
即使二进制文件被删除,该过程仍应继续的期望是正确的,但在macOS
的情况下并不完全正确。 该示例触发了 macOS kernel 内部System Integrity Protection
( SIP
) 机制的副作用,但是在解释到底发生了什么之前,我们需要进行一些实验,这将有助于我们更好地理解整个场景。
为了演示发生了什么,我将示例修改为 9,而不是 fork,在 fork 之后,孩子将打印一条消息“我完成了”,等待 1 秒并通过打印0
作为 PID 退出. 父级将继续计数到 14 并打印子 PID。 代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
for(int i=0; i <10; i++)
{
sleep(1);
printf("%i ", i);
}
pid_t forkResult;
forkResult = fork();
if (forkResult != 0) {
for(int i=10; i < 15; i++) {
sleep(1);
printf("%i ", i);
}
} else {
sleep(1);
printf("I am done ");
}
printf("after fork %d \n", forkResult);
return 0;
}
编译后,我开始了正常的场景:
╰> ./a.out
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 4385
因此,正常情况按预期工作。 我们看到从 0 到 9 的计数两次的事实是由于在 fork 调用中完成的stdout
缓冲区的副本。
现在是做负面场景的时候了,我们将在启动后等待 5 秒并删除二进制文件。
╰> ./a.out & (sleep 5 && rm a.out)
[4] 8555
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 8677
[4] 8555 done ./a.out
我们看到 output 仅来自父级。 由于父母已经数到 14,并且显示了孩子的有效 PID,但是孩子失踪了,它从来没有打印过任何东西。 因此,在执行fork()
之后子创建失败,否则fork()
将收到错误而不是有效的PID
。 来自ktrace
的跟踪显示,孩子是在 pid 下创建并被唤醒的:
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.3 MACH_DISPATCH 1bc 0 84 4 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.2 TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 41 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.0(0.0) TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.0 TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623854 +04 0.0 imp_thread_qos_and_relprio 88775d 20000 20200 6 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623854 +04 0.0 imp_update_thread 88775d 811200 140000100 1f 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.1(0.8) imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0(1.1) imp_thread_qos_and_relprio 88775d 30000 20200 40 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0 imp_thread_qos_workq_override 88775d 30000 20200 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0 imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.1(0.1) imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0(0.2) imp_thread_qos_workq_override 88775d 30000 20200 40 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623857 +04 1.3 TURNSTILE_turnstile_added_to_thread_heap 88775d 9931ba6049ddcc77 0 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623858 +04 1.0 MACH_MKRUNNABLE 88775d 25 0 5 888065 2 a.out(8677)
t
因此,孩子的进程使用MACH_MKRUNNABLE
进行调度,并使用MACH_DISPATCH
使其可运行。 这就是父级在fork()
之后获得有效PID
的原因。
此外,正常情况下的ktrace
显示进程已发出BSC_exit
并且发生了imp_task_terminated
系统调用,这是进程退出的正常方式。 但是,在我们删除文件的第二种情况下,跟踪不显示BSC_exit
。 这意味着子节点被 kernel 终止,而不是正常终止。 我们知道终止发生在子节点被正确创建之后,因为父节点已经收到了有效的 PID 并且 PID 被设置为可运行的。
这使我们更接近于理解这里发生了什么。 但是,在我们得出结论之前,让我们展示另一个更加“扭曲”的例子。
如果我们在启动进程后替换文件系统上的二进制文件怎么办?
这是回答这个问题的测试:我们将启动该过程,删除二进制文件并使用touch
在他的位置创建一个同名的空文件。
╰> ./a.out & (sleep 5 && rm a.out; touch a.out)
[1] 6264
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 6851
[1] + 6722 done ./a.out
等一下,这行得通?? 这里发生了什么??!?
这个奇怪的例子为我们提供了重要的线索,可以帮助我们解释发生了什么。
第三个示例有效而第二个示例失败的原因揭示了这里发生的很多事情。 正如一开始提到的,我们正在绊倒SIP
的副作用,更准确地说是runtime protection
机制。
为保护系统完整性, SIP
将检查运行进程的system protection
和special entitlement
。 来自苹果文档: ...当一个进程启动时,kernel 检查主要可执行文件是否在磁盘上受到保护或使用特殊的系统权利进行签名。 如果其中任何一个为真,则设置一个标志以表示它受到保护以防修改。 kernel 拒绝任何附加到受保护进程的尝试...
当我们从文件系统中删除二进制文件时,保护机制无法识别子进程的类型,也无法识别特殊的系统权利,因为磁盘中缺少二进制文件。 这触发了保护机制将此进程视为系统中的入侵者并终止它,因为我们没有看到子进程的BSC_exit
。
在第三个示例中,当我们使用touch
在文件系统上创建虚拟条目时, SIP
能够检测到这不是特殊进程,也没有特殊权利,并允许进程继续。 这是一个非常可靠的迹象,表明我们在SIP
实时保护机制上跳闸。
为了证明是这种情况,我禁用了需要在恢复模式下重新启动的SIP
并执行了测试
╰> csrutil status
System Integrity Protection status: disabled.
╰> ./a.out & (sleep 5 && rm a.out)
[1] 1504
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 1626
因此,整个问题是由System Integrity Protection
引起的。 更多细节可以在文档中找到
SIP
需要的只是在文件系统上有一个带有进程名称的文件,因此该机制可以运行验证并决定允许子进程继续执行。 这向我们表明,我们正在观察副作用,而不是设计的行为,因为空文件甚至不是有效的dwarf
,但执行已继续。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.