Audit report AR-12
Reported privately in an independent security review, 2026-08-07.
- Report date
- 2026-08-07
- Kind
- Hardening
- Status on master
- Partly fixed in mp#312
- Proposed issue state
- Open
- Linked bugs
- Bug 2: Spawned child process can deadlock before it starts running
Summary
A collection of error-handling weaknesses in libmultiprocess's process helpers: descriptor leaks on some setup failures, no EINTR retry around waitpid, permissive parsing of the descriptor number passed to a spawned child, a non-async-signal-safe diagnostic after fork(), and weak handling of empty argument lists and throwing callbacks. mp#312 fixed the post-fork diagnostic and the fork-failure leak four days after the report; the other items remain. Each is real and small, none is reachable by a peer. Hardening.
The report
The report lists these weaknesses in the process helpers in src/mp/util.cpp, and suggests fixing them in the same pass as the descriptor-inheritance problems of AR-15:
- descriptor leaks on some setup failures;
- no
EINTRretry aroundwaitpidand related system calls; - permissive parsing of the descriptor string passed to the child, which accepts trailing characters;
- diagnostics that are not async-signal-safe between
fork()andexec(); - weak handling of empty argument vectors and of callback failures.
It gives no code or reproduction for these items.
Proposed change
Fix the items in a spawn-hardening pass. No code.
Assessment
Checked against src/mp/util.cpp on libmultiprocess master.
Fixed by mp#312 (merged 2026-08-11):
- At the commit the report examined, the child called
perror("execvp failed")after a failedexecvp, with a comment noting it could deadlock in a multithreaded parent. mp#312 replaced it with an async-signal-safe error report written to a socket the parent reads, and the parent now throws. This was the last remnant of a fixed post-fork deadlock: Spawned IPC child deadlocks between fork() and exec() while building argv. The same class of problem in the code generator is mpgen's fork()ed child allocates before exec and calls perror. SpawnProcessnow closes all its descriptors whenfork()fails, and kills and reaps the child, retryingwaitpidonEINTR, when a later step fails.
Still present on master:
WaitProcesscallswaitpidonce and throws onEINTRinstead of retrying.StartSpawnedparses the descriptor withstd::stoi, which accepts"12abc".SocketPair()leaks both descriptors if eitherfcntlcall throws, andSpawnProcessleaks the first pair if creating the second pair throws.SpawnProcessleaks all four descriptors if theconnect_info_to_argscallback orMakeArgvthrows beforefork().- An empty argument list gives a null
argv[0], andStartProcesscallsargs.front()on it.
The inputs for all of these come from the spawning process itself, not from a peer. On Bitcoin Core master SpawnProcess is reached only through the test-only echoipc RPC; it matters more on the unmerged multiprocess branch, where the GUI spawns the node and the node spawns wallets. An earlier, fixed leak on a different error path is SpawnProcess leaks the parent's end of the socketpair if closing the child's end fails.
Status
Partly fixed by mp#312. The remaining items are a few lines each: an EINTR loop in WaitProcess, std::from_chars with a full-match check in StartSpawned, RAII ownership of the socket pairs until fork(), and an argument-count check. Small libmultiprocess cleanup; no issue open.
Reported privately in an independent security review on 2026-08-07.