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
Fix
bitcoin#36520
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

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

Timeline

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