Bug 98
Process aborts instead of reporting an error when the IPC event loop fails to start
- Status
- Open
- Symptom
- Crash; worst effect seen: crash
- Where seen
- Bitcoin Core unit tests
- Trigger
- Node startup
- First reported
- Reported privately in an independent security review, 2026-09-27
- Fixed
- Not yet
- In released binaries
- Yes (v30.0 onward)
- Defect ever on master
- Yes
Summary
Bitcoin Core starts its IPC event loop on a background thread the first time a process listens for or opens an IPC connection. If creating the loop fails, for example because the process has run out of file descriptors, the error escapes that thread and C++ terminates the whole process with an abort. The caller expects an exception it can report: bitcoin-node would otherwise exit with "Unable to bind to IPC address". It was reported privately in an independent security review, with a test that injects the failure; it has not been seen in a running process, and in most cases the process would have stopped anyway, just with a clear message instead of an abort. No fix is merged yet.
What happened
Nothing has been seen in a deployment. An independent security review reported the problem privately on 2026-09-27. Its unit test throws an exception just before the event loop is constructed and checks that the error reaches the code that asked for the connection. The report says that without a fix the exception left the background thread and the process aborted with SIGABRT. That run has not been repeated independently.
Root cause
CapnpProtocol::startLoop() in src/ipc/capnp/protocol.cpp creates the event loop lazily, on the first connect(), listen() or makeStream() call. It starts a thread that constructs an mp::EventLoop, signals the waiting caller through a std::promise, and then runs the loop. The caller waits for that signal.
The thread's code has no try/catch. The EventLoop constructor sets up Cap'n Proto's asynchronous I/O and creates a socket pair, and either step throws if a system call fails, for example when no file descriptors are left. An exception that escapes a thread's entry function makes C++ call std::terminate, so the process aborts while the caller is still waiting. The callers are written to handle an exception: bitcoin-node wraps its -ipcbind setup in a try/catch that reports "Unable to bind to IPC address", and the interfaces::Ipc methods document that they throw on errors.
Running out of memory is not a practical trigger: the node and GUI already install a handler that terminates on out-of-memory. The difference is for system errors such as descriptor exhaustion.
Fix
No fix merged yet. The fix proposed with the report catches any exception while the loop is being created on the background thread, passes it to the caller through the promise (set_exception), clears the partly built state, and has the caller join the thread and rethrow. The same protocol object can then try again later:
- promise.get_future().wait();
+ try {
+ future.get();
+ } catch (...) {
+ m_loop_thread.join();
+ throw;
+ }
Exceptions after the loop has started are outside that change. The report considered constructing the event loop on the caller's thread instead, which would change which thread owns it.
Next steps
- Decide whether to take the fix in Bitcoin Core; it is small and only touches
src/ipc/capnp/protocol.cpp. - Consider a public test that injects the failure without forking, or a libmultiprocess-level helper so other applications that start a loop on a background thread get the same error path.
Timeline
- 2021-04-27: present since the IPC implementation was added in bitcoin#19160 ("multiprocess: Add basic spawn and IPC support"), with the unguarded thread in
startLoop(). - 2024-09-09: reachable from a released node option once
bitcoin-nodegained-ipcbindin bitcoin#30509. - 2026-09-27: reported privately in an independent security review, with a fault-injection test.
- Releases: v30.0 and later ship
bitcoin-nodewith-ipcbind.
Lessons
What would have prevented this bug, found it sooner, or made it easier to debug. The patterns page groups these lessons across all bugs.
Pattern Error paths that skip cleanup or completion: An exception escaping a thread
- Prevented by a design rule
- Earliest detector startup test (did not exist then)
- Diagnosis easy
- Fix design easy
- Debugging aids a clear assertion message
- Rust partly
Would Rust have prevented it? Partly. A panic or error in a spawned Rust thread does not abort the process; the sending half of the channel is dropped and the waiting caller gets a receive error, so the abort becomes an error by default, but the caller still learns only that startup failed unless the code sends the error across, as the C++ fix does with set_exception. More on Rust
Could it have been prevented?
Yes, with a rule for every thread entry function: catch everything at the top and hand
the error to whoever is waiting. CapnpProtocol::startLoop() in
10afdf0280fa used a
std::promise only to say "ready", so there was no channel for "failed", and the
thread body assumed the EventLoop constructor could not throw, although it makes
system calls that report failure as exceptions. The fix shows the rule: wrap the setup
in try/catch and pass the exception through promise.set_exception().
Could it have been found sooner?
Yes, by a startup test that makes loop construction fail and checks the caller gets an exception. No such test existed: IPC tests start the loop successfully, and real failures need descriptor exhaustion at the moment of the first IPC call. An independent security review found it by looking for exceptions that can cross a thread boundary and confirmed it with an injected failure.
What made it hard to debug?
Little. The C++ runtime prints "terminate called after throwing an instance of ..." with the exception text before aborting, which points at the cause. In a real deployment the confusing part would be an abort during startup where the operator expected an error message such as "Unable to bind to IPC address".
Lesson
Code that starts a thread and waits for it to say it is ready also needs a way for the thread to say it failed; otherwise any exception during setup ends the whole process instead of reaching the caller who could report it.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | crash |
| Worst effect observed | crash |
| Seen in | unit-test |
| Scope | production |
| Trigger | Node startup (node-startup) |
| Phase | startup |
| Side | eventloop |
| Component | bitcoin-ipc |
| Root cause kind | unhandled-exception |
| Fix kind | none |
| Detection | review |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | yes |
| Released | binary |
| Confidence | low |