Hi all, I wanted to gauge opinions on the following issue:
Runtime.exec, on Linux, uses vfork(2) by default. It gives us better performance compared with fork() and robustness in constrained memory situations. But as we know vfork() can be dangerous if used incorrectly. In the child process before exec'ing, we live in the memory of the parent process. If we are not very careful we can influence or crash the parent process. According to POSIX pretty much the only thing the child process is allowed to do after vfork(2) is to exec(3) immediately; if that fails, you must call _exit(2). http://pubs.opengroup.org/onlinepubs/009604599/functions/vfork.html However, in the openjdk we do a number of things beyond that: - stdin,out,err pipe handling business - closing all file descriptors - we change the working directory - we may actually modify errno manually - in case exec fails, we communicate the error back to the parent using pipe. This involves calling a number of libc functions beyond exec(), namely read, close, dup2, opendir/readdir, write, chdir... It also needs a bit of stack, since we assemble path names. -- I was curious whether there were any real issues, so I tested (on Ubuntu 16.4) and found: 1) A crash - any crash - in the child process before exec() will kill the parent jvm dead. Weirdly enough, we do not even enter our error handling, but seem to die instantly with the default "Segmentation Fault". 2) Signals received by the child process before exec() influence the parent process. For example: - SIGINT set to the child ends both parent and child, immediately - SIGABRT aborts both child and parent - any error signal sent to the child lead to the behavior described at (1) 3) A stack overflow in the child before exec() also kills the parent. Unsurprising, since guard page hit -> segfault -> see (1). 4) more amusing, setting errno in the child before exec() changes the errno in the parent process. propagates to the parent process. But since errno is thread local and the thread in the parent process is waiting in vfork() and will, upon return, not look at errno (after all, vfork succeeded) this causes no trouble. There may be more issues, but these were the ones I tested. In all cases I counter-tested with fork() instead of vfork() and as expected with fork() the parent process stays unaffected as it should be. ------------- Whether you think these issues are worth solving is an open question. All these cases may happen in the wild (well, apart from crash-by-programming-error if one assumes the program to be really bug free) albeit with a very small probability. But once these bugs occur, they can be very difficult to analyse. So fixing this may be worthwhile. At SAP, we opted for robustness, so we changed the Runtime.exec() implementation to deal with vfork() issues. Basically, we employ the exec-twice technique: - in the child, after the vfork(), we immediately exec() into a little bootstrap binary ("forkhelper"). - Now we are safe in the sense that we do not share memory with the parent process anymore - Then, parent process communicates with the child via pipes and gives it all information needed to do the "real" exec: environ, current dir, arguments... . - Now the child exec's a second time, this time into the real target binary. The point of this technique is that we minimize the window in the child between vfork and the first exec. In fact, we are now fully POSIX compliant. This solves the described pathological cases. It has some other advantages too, e.g. allowing for better error handling and tracing in the Runtime.exec() area. Performance-wise it depends: we exec twice, so we pay twice. However, since the parent continues execution after the first exec, it spends less time waiting on the child process, which can make a difference if there are many file descriptors open. --- Checking opinions here. Do you think we are okay with our current implementation or would a change as described above be welcome in the OpenJDK too? Thanks, and Best Regards, Thomas