Audit report AR-45
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 69: Spawned process's socket could leak into another child started at the same moment
Summary
While SpawnProcess prepares a child process, both ends of the new IPC socket pair can be inherited by any other program the host process starts at the same moment. That program keeps a copy of the child's socket, so the connection does not see end-of-file until it exits. The report is accurate for the libmultiprocess copy in Bitcoin Core, which never marks these sockets close-on-exec. Current libmultiprocess already fixed the main problem and keeps only a very short window, which the report's use of SOCK_CLOEXEC closes. Same finding as part of AR-15; bug page: Spawned process's socket could leak into another child started at the same moment.
The report
SpawnProcess creates a Unix socket pair and then calls the application's fd_to_args callback to build the child's command line before it forks. Both descriptors stay inheritable during that callback. A test pauses inside the callback, starts a shell from another thread, and shows that the shell kept the child's socket across exec.
The report rates it as hardening. It needs an application that starts an unrelated program while SpawnProcess runs. The inherited socket can delay closing the connection and the synchronous cleanup of the child until that program exits. No ordinary Bitcoin Core sequence, remote trigger or data effect was shown.
Proposed change
Create the pair with SOCK_CLOEXEC where the platform has it, otherwise set close-on-exec on both ends right after creating them, and clear it on the child's end only in the child, after fork():
+#ifdef SOCK_CLOEXEC
+ if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) != 0) {
+#else
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
+#endif
...
if (!pid) {
+ if (!SetCloseOnExec(fds[0], false)) {
+ ...
+ _exit(126);
+ }
On platforms without SOCK_CLOEXEC a short gap between creating the pair and setting the flag remains. The report considered serializing all process creation in the host (a contract outside the library) and replacing fork and exec with another spawn API (a larger portability change).
Assessment
Accurate for Bitcoin Core master. Its libmultiprocess copy (v13) creates the pair with a plain socketpair() and never sets close-on-exec, so the child's end is inheritable until the parent closes it after fork(), and the parent's end for the whole life of the connection. A test against Bitcoin Core master on 2026-09-26 showed both: a sleep started from inside SpawnProcess delayed end-of-file by 3 seconds, and a program started later also inherited the parent's end. The report's test checks only the child's end, but its fix covers both.
On libmultiprocess master this is already fixed: mp#274 sets close-on-exec on both ends right after creating them, and mp#311 clears it on the child's end only inside the child. The report's test would pass there. What remains is the gap between socketpair() and the two fcntl() calls, named in the review of mp#311. Passing SOCK_CLOEXEC closes it on Linux and the BSDs; macOS has no such flag, so a short gap stays there.
Exposure is low. On Bitcoin Core master SpawnProcess is reached only through a test-only RPC, and the other program would be a notification command such as -blocknotify started at the same moment. The effect is a delayed end-of-file and a delayed wait for the child, not a crash.
Status
Present in Bitcoin Core master until its libmultiprocess subtree is updated past mp#274 and mp#311. The remaining SOCK_CLOEXEC change is part of a planned libmultiprocess issue together with AR-15. No public issue or PR yet.
Reported privately in an independent security review on 2026-09-27.