Skip to content

Commit 9d3c2b6

Browse files
committed
Precompute argv before fork in SpawnProcess
Evaluate fd_to_args and build argv in the parent before fork(). In the child between fork() and execvp(), avoid throwing/allocating and use _exit(1) on error. This likely fixes the occasional deadlock in Bitcoin Core functional test, noticed in bitcoin/bitcoin#34187.
1 parent 14e926a commit 9d3c2b6

File tree

2 files changed

+27
-9
lines changed

2 files changed

+27
-9
lines changed

include/mp/util.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,11 @@ using FdToArgsFn = std::function<std::vector<std::string>(int fd)>;
221221

222222
//! Spawn a new process that communicates with the current process over a socket
223223
//! pair. Returns pid through an output argument, and file descriptor for the
224-
//! local side of the socket. Invokes fd_to_args callback with the remote file
225-
//! descriptor number which returns the command line arguments that should be
226-
//! used to execute the process, and which should have the remote file
227-
//! descriptor embedded in whatever format the child process expects.
224+
//! local side of the socket.
225+
//! The fd_to_args callback is invoked in the parent process before fork().
226+
//! It must not rely on child pid/state, and must return the command line
227+
//! arguments that should be used to execute the process. Embed the remote file
228+
//! descriptor number in whatever format the child process expects.
228229
int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args);
229230

230231
//! Call execvp with vector args.

src/mp/util.cpp

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,23 +122,40 @@ int SpawnProcess(int& pid, FdToArgsFn&& fd_to_args)
122122
throw std::system_error(errno, std::system_category(), "socketpair");
123123
}
124124

125+
// Evaluate the callback and build the argv array before forking.
126+
//
127+
// The parent process may be multi-threaded and holding internal library
128+
// locks at fork time. In that case, running code that allocates memory or
129+
// takes locks in the child between fork() and exec() can deadlock
130+
// indefinitely. Precomputing arguments in the parent avoids this.
131+
const std::vector<std::string> args{fd_to_args(fds[0])};
132+
const std::vector<char*> argv{MakeArgv(args)};
133+
125134
pid = fork();
126135
if (pid == -1) {
127136
throw std::system_error(errno, std::system_category(), "fork");
128137
}
129-
// Parent process closes the descriptor for socket 0, child closes the descriptor for socket 1.
138+
// Parent process closes the descriptor for socket 0, child closes the
139+
// descriptor for socket 1. On failure, the parent throws, but the child
140+
// must _exit(1) (post-fork child must not throw).
130141
if (close(fds[pid ? 0 : 1]) != 0) {
131-
throw std::system_error(errno, std::system_category(), "close");
142+
if (pid) throw std::system_error(errno, std::system_category(), "close");
143+
_exit(1);
132144
}
145+
133146
if (!pid) {
134-
// Child process must close all potentially open descriptors, except socket 0.
147+
// Child process must close all potentially open descriptors, except
148+
// socket 0. Do not throw, allocate, or do non-fork-safe work here.
135149
const int maxFd = MaxFd();
136150
for (int fd = 3; fd < maxFd; ++fd) {
137151
if (fd != fds[0]) {
138152
close(fd);
139153
}
140154
}
141-
ExecProcess(fd_to_args(fds[0]));
155+
156+
execvp(argv[0], argv.data());
157+
perror("execvp failed");
158+
_exit(1);
142159
}
143160
return fds[1];
144161
}
@@ -158,7 +175,7 @@ void ExecProcess(const std::vector<std::string>& args)
158175
int WaitProcess(int pid)
159176
{
160177
int status;
161-
if (::waitpid(pid, &status, 0 /* options */) != pid) {
178+
if (::waitpid(pid, &status, /*options=*/0) != pid) {
162179
throw std::system_error(errno, std::system_category(), "waitpid");
163180
}
164181
return status;

0 commit comments

Comments
 (0)