Bug 103
An IPC client's notification handler returning an error aborts the node
- Status
- Open, fix under review
- Symptom
- Crash; worst effect seen: crash
- Where seen
- Branch-build
- First reported
- 2026-10-06 (first report)
- Fixed
- Not yet
- In released binaries
- No
- Defect ever on master
- Yes
Summary
A program that subscribes to chain notifications over the Chain IPC interface could crash bitcoin-node by returning an error from a notification handler such as updatedBlockTip. The error comes back to the node as an ipc::Exception on the thread that delivers validation notifications, nothing catches it there, and the process terminates. A disconnect while a notification is being delivered has the same effect. Only the open PR bitcoin#29409, which exposes Chain over IPC, makes this reachable, so master and releases are not affected. The fix is in bitcoin#36520, not yet merged.
What happened
- @xyzconstant, writing a
ChainNotificationsserver in Rust for peer-observer, made a handler return an error and reported on 2026-10-06 that a node built from bitcoin#29409 aborted withterminating due to uncaught exception of type ipc::Exception: kj::Exception: (remote):0: failed: remote exception: Triggered from peer-observer's ipc-extractor (UpdatedBlockTip), after the thread wrapper printedbitcoin in scheduler. The report included a functional test that reproduces it and suggested catching and logging such errors.
Root cause
NotificationsProxy in src/node/interfaces.cpp adapts a Chain::Notifications object to the node's CValidationInterface. Its methods run on the validation interface scheduler thread and call the handler directly. When the handler lives in another process, a remote error or a disconnect during the call is thrown on the node side as an ipc::Exception. Neither NotificationsProxy nor the scheduler catches it, so it escapes the thread and the node terminates. The code was written for the wallet's in-process handlers, which are not expected to throw.
Fix
No fix is merged yet. In bitcoin#36520, the commit b67d871f95fa, by ryanofsky and written with Claude, sends every notification through a helper that catches ipc::Exception and logs "Chain notification ... failed". Other exceptions are still not caught, since an in-process handler throwing indicates a bug. The handler stays registered after an error: as ryanofsky replied, a client that disconnects loses its handler along with the connection, and a client that fails one notification may still want the others. A new throwingNotifications unit test checks that later notifications are still delivered after a handler throws.
Next steps
- Review and merge bitcoin#36520, ideally before bitcoin#29409 exposes
Chainover IPC. - Consider adding the functional test from the report, which drives a real IPC client, to bitcoin#29409's tests.
Timeline
- 2026-10-06: reported by @xyzconstant on bitcoin#29409.
- 2026-10-14: bitcoin#36520 opened; still open.
- Releases: not affected. On master,
Chainis not exposed over IPC.
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: Assuming a remote call cannot fail
- Prevented by a design rule
- Earliest detector a test with a non-C++ client (did not exist then)
- Diagnosis easy
- Fix design moderate
- Debugging aids logging
- Rust no
Would Rust have prevented it? No. An error from a remote handler propagating out of a scheduler thread is error handling, not memory or thread safety; Rust would make the Result visible at the call site, but a .unwrap() or ? into a thread that panics on error ends the same way. More on Rust
Could it have been prevented?
Yes, with the rule that every call into another process can fail, and that the code
making it must decide what a failure means. NotificationsProxy calls
Chain::Notifications methods from the validation scheduler thread with no error
handling, because the wallet's in-process handlers do not throw. Once
bitcoin#29409 lets another process
implement those handlers, each call can throw ipc::Exception on a remote error or a
disconnect. The fix in bitcoin#36520
applies the rule at the one place all notifications pass through. The same assumption
caused earlier crashes on the same branch, in a destroy call made from a destructor
and in ChainClient::stop() at shutdown, so the rule was known; it had not been applied
to the notification path.
Could it have been found sooner?
Yes, by a test with a notification handler that fails, which the fix adds as a unit test and the report supplied as a functional test. It was found by xyzconstant while writing a Rust client that returns errors from handlers, a behaviour the wallet never has.
What made it hard to debug?
Little: the log shows the remote error text, the exception type and the thread name ("bitcoin in scheduler") before the abort, which points straight at the notification path.
Lesson
Code that delivers callbacks to plug-ins in other processes must treat each callback as a call that can fail, and contain that failure on the delivering thread, because an error from one subscriber should never stop the publisher.
Notes
- Earlier bugs on the same branch had the same shape, a call to a client that was assumed not to fail: a
destroycall throwing in a destructor andChainClient::stop()throwing at shutdown.
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 | branch-build |
| Scope | production |
| Trigger | other (other) |
| Phase | steady-state |
| Side | server |
| Component | bitcoin-ipc |
| Root cause kind | unhandled-exception |
| Fix kind | local-fix |
| Detection | manual-test |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | yes |
| Released | no |
| Confidence | high |