Linux, .NET/C#: After fork(), parent process terminates when child calls exec()

1 week ago 13
ARTICLE AD BOX

I am trying to write code that hosts a child process in a PTY. Doing this is not hugely more involved than just redirecting stdin and stdout, but there's a bit to it. Per the documentation:

You call openpty, which finds a free PTY and returns the "master" and "slave" file descriptors for its bidirectional pipe.

You fork, creating the child process.

In the parent process, fork returns the child process ID. The parent process can then read from the master FD to observe output from the process and write to it to send input.

In the child process, fork returns 0. The child process now has some specific tasks to perform:

The default stdin, stdout and stderr FDs need to be closed.

dup2 is used to insert the slave FD from the PTY into STDIN_FILENO, STDOUT_FILENO and STDERR_FILENO.

Finally, a variant of exec is used to replace the child process with the desired other program.

.NET's managed environment isn't terribly compatible with this sequence, because there are all sorts of management threads, such as garbage collection, and the fork only clones the thread that called it. But, I have been writing code based on a hopeful assumption that if I restrict myself to direct, simple P/Invoke calls and avoid any marshalling and exceptions, then the JIT output that needs to run will be pretty straightforward and should work, up until the process gets replaced.

As far as I can tell, this is in fact the case. In my observations, the P/Invoke calls to close and dup2 between fork and exec all work just fine. But, as soon as the child process calls exec (I am using the execvp variant, with the arguments pre-marshalled before fork just in case), the parent process dies. I don't see any explanation; the debugger simply says, The program '[3744333] QBX.dll' has exited with code 0 (0x0).

My initial thought was that there must be some signals happening and somehow the parent process is picking up on those. This seemed to be confirmed when I read documentation in the runtime codebase that implied that signals were caught with a tiny stub handler that simply writes notification of the event to a pipe, for it to be picked up by a signal processing thread elsewhere. If the child process gets a copy of the pipe handles, then whatever it writes would be picked up by the parent.

The implementation of SystemNative_ForkAndExecProcess in the runtime contains some additional code which clears the signal handlers before fork. Oddly, it seems to restore the configuration after the fork and before exec. In any case, I replicated exactly what SystemNative_ForkAndExecProcess does, but my parent process still dies as soon as the child runs exec. If I replace the exec call from the child process with Environment.Exit, then the parent obviously doesn't have any child process to interact with, but also it doesn't exit.

How do I prevent the parent process from exiting when the child calls exec? It must be possible, because the framework does it.

Read Entire Article