Bug 26
Node started with -disablewallet aborts when a GUI with wallets enabled connects
- Status
- Open
- Symptom
- Assertion failure; worst effect seen: assertion failure
- Where seen
- Branch-build
- Trigger
- GUI startup
- First reported
- 2025-03-27 (first report)
- Fixed
- Not yet
- In released binaries
- Yes
- Defect ever on master
- Yes
Summary
A bitcoin-node started with -disablewallet aborts on an Assert in NodeImpl::walletLoader when a bitcoin-gui started without -disablewallet connects to it over IPC, and the GUI hangs. The GUI asks the node for its wallet loader, and the node asserts that it has one. That was safe when the GUI and node ran in one process with one set of options, but over IPC the two processes can be configured differently. The trigger needs bitcoin-gui -ipcconnect, which only exists in the unmerged bitcoin#19461, so no release is affected, but the asserting line is still on master. No fix yet.
What happened
- @jimhashhq, testing the rebased branch of bitcoin#19461 ("multiprocess: Add bitcoin-gui -ipcconnect option"), reported (2025-03-27) that running
bitcoin-nodewith-disablewalletand connecting abitcoin-guiwithout it using-ipcconnect=automade the node die ("Segmentation fault (core dumped)") and the GUI hang. - ryanofsky acknowledged it and opened mp#169 ("Bitcoin crash when -ipcconnect and -disablewallet are used together", 2025-04-07) to track it. His reproduction with
bitcoin-node -regtest -debug=ipc -disablewallet -ipcbind=unixandbitcoin-gui -regtest -debug=ipc -printtoconsole -ipcconnect=unixshows the node failing withnode/interfaces.cpp:377 walletLoader: Assertion `m_context->wallet_loader' failed.in aNodeImpl::walletLoadercall on a worker thread created for the GUI, while the GUI hangs. The original report is inconsistent about which process crashed (its shell output namesbitcoin-gui, its textbitcoin-node); in the reproduction it is the node that aborts.
Root cause
interfaces::Node::walletLoader() (src/interfaces/node.h, implemented in src/node/interfaces.cpp) returns a reference and asserts that NodeContext::wallet_loader exists:
WalletLoader& walletLoader() override { return *Assert(m_context->wallet_loader); }
In-process this is safe: bitcoin-qt decides whether to use the wallet from the same arguments that decide whether the node creates a WalletLoader, so caller and callee cannot disagree. Over IPC the GUI is a separate process with its own arguments. A GUI that was not given -disablewallet calls walletLoader on a node that has no loader, and the Assert turns a configuration mismatch into a node crash. By comparison, bitcoin-cli wallet RPCs against the same node just return "Method not found".
The assertion predates IPC. bitcoin#19461 builds on bitcoin#19460 ("multiprocess: Add bitcoin-wallet -ipcconnect option"), which also has bitcoin-node listen on a socket with -ipcbind. It is the first code that lets a GUI process connect to a node process it did not spawn, so it is the first place the two can be configured differently.
Fix
No fix yet. In mp#169 ryanofsky proposed either changing walletLoader() to return a pointer (or throw) so the GUI can handle a missing loader, or having the GUI check the node's -disablewallet setting first while still making the IPC method throw instead of asserting. He described the longer-term fix as having bitcoin-gui talk to bitcoin-wallet processes directly, after which bitcoin-node -disablewallet could be dropped.
Next steps
- Pick one of the fixes ryanofsky proposed in mp#169. One is to make
walletLoader()return a pointer, or throw, so the GUI can handle a node with no wallet loader. The other is to have the GUI check the node's-disablewalletsetting first, and still make the IPC method throw instead of asserting. - Land the fix before or with bitcoin#19461, the first code that can trigger the crash. Add a regression test based on the mp#169 reproduction:
bitcoin-node -disablewallet -ipcbind=unixwith abitcoin-gui -ipcconnect=unixthat has wallets enabled. - Longer term, having
bitcoin-guitalk tobitcoin-walletprocesses directly would allowbitcoin-node -disablewalletto be dropped, as ryanofsky described in mp#169.
Timeline
- 2020-08-31: the assertion, then in
walletClient(), added to the in-processNodeinterface by e4f43504712 in bitcoin#19099 ("refactor: Move wallet methods out of chain.h and node.h"). It was harmless there. - 2025-03-27: reported by @jimhashhq on bitcoin#19461.
- 2025-04-07: reproduced by ryanofsky and filed as mp#169.
- Still open.
NodeImpl::walletLoaderon Bitcoin Core master still asserts, and bitcoin#19461 is still open, so no release can reach it.
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 An in-process interface trusting what now comes from another process: An assertion on what is now input from a peer
- Prevented by a design rule, a different API shape
- Earliest detector code review (did not exist then)
- Diagnosis easy
- Fix design easy
- Debugging aids a clear assertion message
- Rust no
Would Rust have prevented it? No. Asserting that the wallet loader exists is an API contract choice; a Rust walletLoader() returning Option would push the caller to handle absence, but an expect() there is just as idiomatic and would panic the same way. More on Rust
Could it have been prevented?
Yes. Node::walletLoader() returns a reference and asserts that the node has a
WalletLoader, which in-process is a true precondition: bitcoin-qt decides whether
to ask for the wallet from the same arguments that decide whether the node builds one.
Once Node is served over IPC to a process with its own arguments, "the node has a
wallet loader" is a fact about the peer, and an Assert on a fact about the peer is a
remote crash. The rule is the one the
bitcoin#33222 case also produced: at
an interface that can be reached from another process, preconditions on arguments or
on the callee's configuration become validation, returning null or throwing. The API
shape ryanofsky proposed,
a pointer return or a thrown error the GUI can handle, is what an interface designed
for a remote caller would have had from the start. This was knowable when
bitcoin#19461 first let a GUI connect
to a node it did not spawn, since that is the first moment the two could be configured
differently.
Could it have been found sooner?
Yes, by review rather than by a test: the audit "for each method served over IPC, list
every Assert reachable from its arguments or from state the caller cannot see" is
mechanical and finds this line without running anything. A test would need the
configuration matrix (node with -disablewallet, GUI without), which no existing test
generates; bitcoin-cli wallet RPCs against the same node already returned "Method not
found", so the in-tree precedent for the right behaviour existed. The assertion was
added in 2020 for an in-process interface and was correct there; nobody re-read it when
the interface went over the wire, which is the usual way this pattern survives.
What made it hard to debug?
Nothing. The report gave the two command lines, ryanofsky reproduced it with -regtest
and the assertion text names the file, line and the missing member. The only confusion
was in the report itself, whose shell output and prose disagreed about which process
died; the reproduction settled it as the node. The fix is unwritten because the branch
that exposes it is unmerged, not because it is hard.
Lesson
When an interface starts serving callers from another process, every assertion about the callee's own configuration becomes an assertion about the peer's guess at it; return an error a remote caller can act on, and audit the interface's asserts the day it goes over the wire.
Notes
- Although filed in the libmultiprocess tracker, this is a Bitcoin Core IPC interface issue rather than a libmultiprocess bug, as mp#169 says.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | assert |
| Worst effect observed | assert |
| Seen in | branch-build |
| Scope | production |
| Trigger | GUI startup (qt-startup) |
| Phase | steady-state |
| Side | server |
| Component | bitcoin-ipc |
| Root cause kind | assumption-violated |
| Fix kind | none |
| Detection | user-report |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | yes |
| Released | binary |
| Confidence | high |