Audit report AR-66

Reported privately in an independent security review, 2026-09-27.

Report date
2026-09-27
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 91: Signal during spawned-process cleanup can terminate the parent

Summary

libmultiprocess's WaitProcess gives up if a signal interrupts its waitpid call (EINTR), although the right response is to call it again. The report describes the result as a child process that is never reaped. In Bitcoin Core the outcome is worse: the wait runs as a cleanup step inside a destructor that cannot throw, so the error terminates the process. The trigger is narrow, and on Bitcoin Core master only a test-only RPC spawns processes. The fix, retrying on EINTR, is right. Bug page: Signal during spawned-process cleanup can terminate the parent.

The report

When an IPC parent is done with a spawned child, it waits for the child to exit so the operating system can release the child's process entry. WaitProcess calls waitpid once and throws for any result other than the child's pid. If a signal handler installed without SA_RESTART runs while the call is blocked, waitpid fails with EINTR, which only means "try again", and WaitProcess treats it as a final failure. Bitcoin Core registers this wait as the cleanup step for each spawned process and has no other reaper.

The reproduction is a Bitcoin Core unit test. It starts a child that waits on a pipe, installs a SIGUSR1 handler without SA_RESTART, waits for the child on a second thread, and sends SIGUSR1 to that thread repeatedly. Without the fix the wait returns early with EINTR while the child is still running; with it the wait returns the child's exit status.

The report rates it low. A local signal must reach the waiting thread during the call. The demonstrated effect is a child left unreaped until the parent exits. No IPC message, remote trigger, persistent state or funds effect was shown.

Proposed change

Retry waitpid only when it fails with EINTR, and keep throwing every other error:

 int WaitProcess(int pid)
 {
     int status;
-    if (::waitpid(pid, &status, /*options=*/0) != pid) {
+    int result;
+    do {
+        result = ::waitpid(pid, &status, /*options=*/0);
+    } while (result < 0 && errno == EINTR);
+    if (result != pid) {
         throw std::system_error(errno, std::system_category(), "waitpid");
     }
     return status;

The report rejected adding SA_RESTART to Bitcoin Core's handlers, since that would not protect other users of the library or other signals, and rejected retrying only in Bitcoin Core's wrapper, since the library function would stay exposed.

Assessment

Accurate about the code, on both libmultiprocess master and Bitcoin Core's copy. libmultiprocess master already retries EINTR in the helper that kills and reaps a child after a failed spawn, so the library is inconsistent with itself. A 2026-08 private audit of libmultiprocess listed the same missing retry as a minor error-handling gap (AR-12) without describing an effect.

The report understates the effect in Bitcoin Core. Ipc::spawnProcess attaches the wait to the client object for the spawned process, and those cleanup steps run from the client's destructor, which cannot throw. An exception there terminates the process, so a node that hits this aborts rather than leaving a zombie. The report says a cleanup change in AR-63 already contained that abort; that change covers a different path (cleanup on the server side), so it does not.

The trigger is narrow. Bitcoin Core installs its SIGTERM, SIGINT and SIGHUP handlers without SA_RESTART, so any of them can interrupt the wait. But on Linux a signal sent to the process is normally handled by the main thread, not the thread that is waiting, so it needs a second signal to arrive while the first is still pending, or a signal aimed at that thread as in the test. On Bitcoin Core master only the test-only echoipc RPC spawns a process, and the window is the moment between the spawned process disconnecting and exiting. A future release that spawns processes for normal operation would run this wait at shutdown, which is when SIGTERM and SIGINT arrive.

The test needs repeated pthread_kill calls inside the multithreaded test_bitcoin. A libmultiprocess spawn test would be a better home for it.

Status

Present on both libmultiprocess master and Bitcoin Core master. Already listed in a planned libmultiprocess issue about process-helper error handling (AR-12). No public issue or PR yet.

Reported privately in an independent security review on 2026-09-27.