简体   繁体   English

如果删除了运行它的可执行文件,为什么 fork() 在 MacOs Big Sur 上会失败?

[英]Why does fork() fail on MacOs Big Sur if the executable that runs it is deleted?

If a running process's executable is deleted, I've noticed fork fails where the child process is never executed.如果删除正在运行的进程的可执行文件,我注意到fork失败,子进程永远不会执行。

For example, consider the code below:例如,考虑下面的代码:

#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;
}

If I compile this and delete the resulting executable before fork is called, I never see fork return a pid of 0, meaning the child process never starts.如果我在调用fork之前编译它并删除生成的可执行文件,我永远不会看到fork返回 0 的 pid,这意味着子进程永远不会启动。 I only have a Mac running Big Sur, so not sure if this repros on other OS's.我只有一台运行 Big Sur 的 Mac,所以不确定这是否适用于其他操作系统。

Does anyone know why this would be?有谁知道为什么会这样? My understanding is an executable should work just fine even if it's deleted while still running.我的理解是一个可执行文件应该可以正常工作,即使它在运行时被删除。

The expectation that the process should continue even if the binary was deleted is correct, however not fully correct in case of macOS .即使二进制文件被删除,该过程仍应继续的期望是正确的,但在macOS的情况下并不完全正确。 The example is tripping on a side-effect of the System Integrity Protection ( SIP ) mechanism inside the macOS kernel, however before explaining what is exactly going on, we need to make several experiments which will help us to better understand the whole scenario.该示例触发了 macOS kernel 内部System Integrity Protection ( SIP ) 机制的副作用,但是在解释到底发生了什么之前,我们需要进行一些实验,这将有助于我们更好地理解整个场景。

Modified example to better demonstrate the issue修改示例以更好地演示该问题

To demonstrate what is going on, I had modified the example to count to 9, than do the fork, after the fork, the child will print a message "I am done", wait 1 second and exit by printing the 0 as the PID.为了演示发生了什么,我将示例修改为 9,而不是 fork,在 fork 之后,孩子将打印一条消息“我完成了”,等待 1 秒并通过打印0作为 PID 退出. The parent will continue to count to 14 and print the child PID.父级将继续计数到 14 并打印子 PID。 The code is as follows:代码如下:

#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;
}

After compiling it, I have started the normal scenario:编译后,我开始了正常的场景:

╰> ./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

So, the normal scenario works as expected.因此,正常情况按预期工作。 The fact that we see the count from 0 to 9 two times, is due to the copy of the buffers for stdout that was done in the fork call.我们看到从 0 到 9 的计数两次的事实是由于在 fork 调用中完成的stdout缓冲区的副本。

Tracing the failing example追踪失败的例子

Now is time to do the negative scenario, we will wait for 5 seconds after the start and remove the binary.现在是做负面场景的时候了,我们将在启动后等待 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

We see that the output is only from the parent.我们看到 output 仅来自父级。 Since the parent had counted to 14, and shows valid PID for the child, however the child is missing, it never printed anything.由于父母已经数到 14,并且显示了孩子的有效 PID,但是孩子失踪了,它从来没有打印过任何东西。 So, the child creation failed after the fork() was performed, otherwise fork() would have received and error instead of a valid PID .因此,在执行fork()之后子创建失败,否则fork()将收到错误而不是有效的PID Traces from ktrace reveal that the child was created under the pid and was waken up:来自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

So the child's process was dispatched with MACH_DISPATCH and made runnable with MACH_MKRUNNABLE .因此,孩子的进程使用MACH_MKRUNNABLE进行调度,并使用MACH_DISPATCH使其可运行。 This is the reason the parent got valid PID after the fork() .这就是父级在fork()之后获得有效PID的原因。

Further more the ktrace for the normal scenario shows that the process had issued BSC_exit and and imp_task_terminated system call occurred, which is the normal way for a process to exit.此外,正常情况下的ktrace显示进程已发出BSC_exit并且发生了imp_task_terminated系统调用,这是进程退出的正常方式。 However, in the second scenario where we had deleted the file, the trace doesn't show BSC_exit .但是,在我们删除文件的第二种情况下,跟踪不显示BSC_exit This means that the child was terminated by the kernel, not by a normal termination.这意味着子节点被 kernel 终止,而不是正常终止。 And we know that the termination happend after the child was created properly, since the parent had received the valid PID and the PID was made runnable.我们知道终止发生在子节点被正确创建之后,因为父节点已经收到了有效的 PID 并且 PID 被设置为可运行的。

This bring us closer to the understanding of what is going on here.这使我们更接近于理解这里发生了什么。 But, before we have the conclusion, let's show another even more "twisted" example.但是,在我们得出结论之前,让我们展示另一个更加“扭曲”的例子。

Even more strange example更奇怪的例子

What if we replace the binary on the filesystem after we started the process?如果我们在启动进程后替换文件系统上的二进制文件怎么办?

Here is the test to answer this question: we will start the process, remove the binary and create an empty file with the same name on his place with touch .这是回答这个问题的测试:我们将启动该过程,删除二进制文件并使用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

Wait a minute, this works??等一下,这行得通?? What is going on here??!?这里发生了什么??!?

This strange example gives us important clue that will help us to explain what is going on.这个奇怪的例子为我们提供了重要的线索,可以帮助我们解释发生了什么。

The root-cause of the issue问题的根本原因

The reason why the third example works, while the second one is failing, reveals a lot of what is going on here.第三个示例有效而第二个示例失败的原因揭示了这里发生的很多事情。 As mentioned on the beginning, we are tripping on a side-effect of SIP , more precisely on the runtime protection mechanism.正如一开始提到的,我们正在绊倒SIP的副作用,更准确地说是runtime protection机制。

To protect the system integrity, SIP will examine the running processes for the system protection and special entitlement .为保护系统完整性, SIP将检查运行进程的system protectionspecial entitlement From the apple documentation: ...When a process is started, the kernel checks to see whether the main executable is protected on disk or is signed with an special system entitlement.来自苹果文档: ...当一个进程启动时,kernel 检查主要可执行文件是否在磁盘上受到保护或使用特殊的系统权利进行签名。 If either is true, then a flag is set to denote that it is protected against modification.如果其中任何一个为真,则设置一个标志以表示它受到保护以防修改。 Any attempt to attach to a protected process is denied by the kernel... kernel 拒绝任何附加到受保护进程的尝试...

When we had removed the binary from the filesystem, the protection mechanism was not able to identify the type of process for the child nor the special system entitlements since the binary file was missing from the disk.当我们从文件系统中删除二进制文件时,保护机制无法识别子进程的类型,也无法识别特殊的系统权利,因为磁盘中缺少二进制文件。 This triggered the protection mechanism to treat this process as an intruder in the system and terminate it, hanse we had not seen the BSC_exit for the child process.这触发了保护机制将此进程视为系统中的入侵者并终止它,因为我们没有看到子进程的BSC_exit

In the third example, when we created dummy entry on the file system with touch , the SIP was able to detect that this is not a special process nor it has special entitlements and allowed the process to continue.在第三个示例中,当我们使用touch在文件系统上创建虚拟条目时, SIP能够检测到这不是特殊进程,也没有特殊权利,并允许进程继续。 This is a very solid indication that we ware tripping on the SIP realtime protection mechanism.这是一个非常可靠的迹象,表明我们在SIP实时保护机制上跳闸。

To prove that this is the case, I have disabled the SIP which requires a restart in the recovery mode and executed the test为了证明是这种情况,我禁用了需要在恢复模式下重新启动的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

Conclusion结论

So, the whole issue was caused by the System Integrity Protection .因此,整个问题是由System Integrity Protection引起的。 More details can be fond in the documentation更多细节可以在文档中找到

All the SIP needed was to have a file on the filesystem with the process name, so the mechanism can run the verification and decide to allow the child to continue the execution. SIP需要的只是在文件系统上有一个带有进程名称的文件,因此该机制可以运行验证并决定允许子进程继续执行。 This is showing us that we are observing a side-effect, rather than designed behavior, since the empty file was not even a valid dwarf , yet the execution had proceed.这向我们表明,我们正在观察副作用,而不是设计的行为,因为空文件甚至不是有效的dwarf ,但执行已继续。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM