Audit report AR-42

Reported privately in an independent security review, 2026-09-26.

Report date
2026-09-26
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 94: checkBlock before genesis is connected aborts the node

Summary

A mining client that calls Mining.checkBlock while the node is starting on an empty data directory or reindexing, after the mining interface is available but before the genesis block is connected, makes bitcoin-node abort on an assertion that the chain has a tip. The report is accurate and its one-line fix, returning an error to the client when there is no tip yet, is the right shape. Bug page: checkBlock before genesis is connected aborts the node.

The report

The mining interface becomes available once the chainstate is loaded, but on a fresh data directory or a reindex the initial block import thread may not have connected genesis yet. In that window checkBlock calls TestBlockValidity(), which asserts that the active chain has a tip, and the node aborts.

The reproduction is a case in interface_ipc_mining.py. It wipes the node's chain directory, restarts it with -loadblock pointing at a named pipe (FIFO) so the import thread blocks before it connects genesis, connects a client, checks that getTip returns no tip, and calls checkBlock with a block built on genesis. The report says the unpatched node aborted on that call.

The report rates it low. The caller needs access to the local mining interface and must call during the startup window with no tip. One call stops the node; restarting it recovers, as long as the call is not repeated before genesis is connected. There is no peer-to-peer or RPC trigger.

Proposed change

Check for a tip under the lock checkBlock already holds, and throw an error to the client instead of reaching the assertion:

     bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) override
     {
         LOCK(chainman().GetMutex());
+        if (!chainman().ActiveChain().Tip()) throw std::runtime_error{"Cannot check a block before the active chain has a tip"};
         BlockValidationState state{TestBlockValidity(chainman().ActiveChainstate(), block, /*check_pow=*/options.check_pow, /*check_merkle_root=*/options.check_merkle_root)};

The client sees a FAILED exception and can retry once getTip reports a tip. The report considered waiting for genesis, as createNewBlock does (rejected, since a stalled import would leave the call blocked), and returning false (rejected, since it would make an unchecked block look invalid).

Assessment

Accurate. On Bitcoin Core master, MinerImpl::checkBlock (src/node/interfaces.cpp) calls TestBlockValidity() (src/validation.cpp), which begins with Assert(chainstate.m_chain.Tip()); Assert aborts in every build. Since bitcoin#34661 (merged 2026-02-27), makeMining waits until the chainstate is loaded, not until there is a tip. bitcoin-node starts its IPC listeners before loading the chain, and on an empty data directory, -reindex or -reindex-chainstate it connects genesis later, in the initial block import thread, after reading any -loadblock files.

The window is usually short: without -loadblock genesis is connected almost at once, and a reindex connects it as soon as it reads it. With -loadblock files the window lasts until they have all been read, and the report's FIFO holds it open. The other mining calls already handle the window: getTip returns no tip, and waitTipChanged and createNewBlock wait for one (from bitcoin#31785). checkBlock, added afterwards in bitcoin#31981, did not get the same treatment. The report's test run was not repeated here; the mechanism follows directly from the code, and the test is a clean way to show it.

Status

Present on master. Next step: a Bitcoin Core PR with the tip check and the functional test. No public issue or PR yet.

Reported privately in an independent security review on 2026-09-26.