Bug 90
Chain IPC calls with an unset transaction would crash the node
- Status
- Open, fix under review
- Symptom
- Crash; worst effect seen: nothing observed
- Where seen
- Never observed
- Trigger
- Unusual input from a peer or caller
- First reported
- 2026-05-20 (first report)
- Fixed
- Not yet
- In released binaries
- No
- Defect ever on master
- Yes
Summary
If a client of the Chain IPC interface called broadcastTransaction or checkChainLimits without setting the transaction argument, bitcoin-node would dereference a null pointer and crash. Cap'n Proto lets a client leave any field unset, and libmultiprocess turns an unset transaction into a null pointer, which the node code never expected because the in-process wallet always passes a transaction. It was found by reading the code and never triggered. Only the open PR bitcoin#29409, which exposes Chain over IPC, makes it reachable, so master and releases are not affected. The fix is in bitcoin#36520, not yet merged.
What happened
- Reviewing the
Chaininterface definition in bitcoin#29409 on 2026-05-20, @willcl-ark asked what happens if thetxargument ofbroadcastTransactionis null, "allowed by capnp spec AFAIU", and expected a segfault. Minutes later he raised the same question forcheckChainLimits. - ryanofsky agreed that this is a general issue for non-C++ clients, since every Cap'n Proto field has an implicit default, and confirmed the
checkChainLimitscase, noting that a C++ client passing a null pointer reaches it too.
Root cause
When the node receives a Data field that the client did not set, libmultiprocess reads it as a null CTransactionRef. ChainImpl::broadcastTransaction() in src/node/interfaces.cpp passes it to BroadcastTransaction(), which immediately calls tx->GetHash(). ChainImpl::checkChainLimits() passes it to CTxMemPool::CheckPolicyLimits(), which hands it to ChangeSet::StageAddition(). Both dereference null. The C++ interface assumes a non-null transaction, as every in-process caller provides one.
Fix
No fix is merged yet. In bitcoin#36520, the commit 42fcc504e21b, by ryanofsky and written with Claude, makes broadcastTransaction return false with the error "Transaction is null" and checkChainLimits return an error. The invalidArguments unit test calls both with nullptr.
The same commit also makes broadcastTransaction reject an out-of-range broadcastMethod value, which willcl-ark raised in the same thread. That case did not crash: the value silently did nothing and the call reported success. It also fixes the getBlockHash assert.
Next steps
- Review and merge bitcoin#36520, ideally before bitcoin#29409 exposes
Chainover IPC. - Look for other
Chainmethods that take a transaction or other pointer argument without a null check.
Timeline
- 2026-05-20: raised by @willcl-ark in review of 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 An in-process interface trusting what now comes from another process: An assertion on what is now input from a peer
- Prevented by a different API shape
- Earliest detector code review (existed then)
- Diagnosis easy
- Fix design easy
- Rust yes
Would Rust have prevented it? Yes. A Rust interface would take the transaction by value or as Arc<Transaction>, which cannot be null; an optional wire field would deserialize to an Option the code must handle, so the null dereference cannot compile. More on Rust
Could it have been prevented?
Yes. The C++ signatures take a CTransactionRef, a shared pointer that can be null,
and in-process callers never pass null, so the code never checks. Over Cap'n Proto
every field is optional and an unset Data field arrives as a null pointer, as
ryanofsky explained:
"in capnproto, all parameters are optional because fields without explicit defaults have
implicit defaults of 0 or null". Either the generated server code could reject unset
fields that map to non-nullable arguments, or each exposed method checks; the fix in
bitcoin#36520 does the second for these
two methods.
Could it have been found sooner?
It was found in review, before any client sent such a call: willcl-ark asked about the unset field while reading the interface definition. A test that calls every exposed method with all fields unset would have found it mechanically, and would find any remaining cases.
What made it hard to debug?
It never crashed, so there was nothing to debug.
Lesson
A serialization format where every field is optional turns every non-null pointer argument into one that can be null; check at the boundary, or generate code that rejects missing required fields, before the call reaches code written for trusted callers.
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 | none |
| Seen in | never |
| Scope | production |
| Trigger | Unusual input (unusual-input) |
| Phase | steady-state |
| Side | server |
| Component | bitcoin-ipc |
| Root cause kind | assumption-violated |
| Fix kind | local-fix |
| Detection | review |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | yes |
| Released | no |
| Confidence | high |