Audit report AR-70
Reported privately in an independent security review, 2026-09-28.
- Report date
- 2026-09-28
- Kind
- Bug
- Status on master
- Present
- Proposed issue state
- Closed
- Linked bugs
- Bug 69: Spawned process's socket could leak into another child started at the same moment
Summary
In Bitcoin Core's copy of libmultiprocess, the parent's end of the socket that SpawnProcess creates for a child process is never marked close-on-exec, so any other program the parent starts while the connection is open inherits a copy, and the connection does not see end-of-file until that program exits. The report is accurate for Bitcoin Core master and says itself that current libmultiprocess already fixed it, in mp#274 and mp#311. It is a narrower form of AR-45. Bug page: Spawned process's socket could leak into another child started at the same moment.
The report
SpawnProcess() creates a socket pair with a plain socketpair() call, gives one end to the child and keeps the other. The parent closes the child's end after forking, but its own end has no close-on-exec flag, so another thread that starts a program at the same time passes a copy of it to that program. The connection then stays open after the parent closes its copy, until the other program closes it or exits. A libmultiprocess test checks that the returned descriptor has the close-on-exec flag.
The report rates it low. It needs the application to start an unrelated program while a spawned process's connection is open. The effect is a delayed end-of-file. The report says an earlier Linux test saw the descriptor in an unrelated shell but that it was not rerun on the final code. No remote trigger or lasting outage was shown.
Proposed change
Set close-on-exec on the parent's end right after creating the pair, before forking, and close both ends if that fails:
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
throw std::system_error(errno, std::system_category(), "socketpair");
}
+ // An unrelated exec must not retain the parent's end of the IPC socket
+ const int flags{::fcntl(fds[1], F_GETFD)};
+ if (flags == -1 || ::fcntl(fds[1], F_SETFD, flags | FD_CLOEXEC) == -1) {
+ ...
+ throw std::system_error(err, std::system_category(), "fcntl");
+ }
The report notes that current libmultiprocess marks both ends instead, and that taking that version with a library update may be preferable.
Assessment
Accurate for Bitcoin Core master, and already fixed upstream. Bitcoin Core master's copy never sets close-on-exec on either end; a test against Bitcoin Core master on 2026-09-26 showed a later child inheriting the parent's end. In libmultiprocess master, mp#274 marks both ends close-on-exec right after creating them and mp#311 clears the flag on the child's end only inside the child. Bitcoin Core's copy of the library was last updated on 2026-07-14, before both.
The patch is narrower than the upstream fix and than AR-45: the child's end stays inheritable from creation until the parent closes it after forking, which is the window AR-45 demonstrated. Both reports describe the same bug. The effect is a delayed end-of-file and a delayed wait for the child, not a crash; 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.
Status
Fixed in libmultiprocess master by mp#274 and mp#311; present in Bitcoin Core master until its libmultiprocess copy is updated. The only remaining upstream change, creating the pair with SOCK_CLOEXEC, is covered by AR-45.
Reported privately in an independent security review on 2026-09-28.