Audit report AR-44

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

Report date
2026-09-27
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 99: One mining interrupt can leave a second wait blocked

Summary

Mining.interrupt() and BlockTemplate.interruptWait() are meant to cancel a client's long waits, but each stores the request in a single flag that the first waiter to wake clears. If a client has two waits running on the same object, one interrupt releases only one of them; the other stays blocked until the next block, its timeout, or another interrupt. The report is accurate. Bug page: One mining interrupt can leave a second wait blocked.

The report

The Mining and BlockTemplate capabilities each keep one boolean interrupt flag. An interrupt sets the flag and wakes every waiter. The first waiter to run clears the flag while holding the lock, so another waiter that checks afterwards finds it cleared and goes back to waiting.

The reproduction is a functional test with a Python client that runs two server worker threads. It starts two waits on one object, sends one interrupt and checks that both return. The report says this failed for two waitTipChanged calls, for two waitNext calls, and for createNewBlock together with waitTipChanged.

The report rates it low. It needs the local mining interface and two concurrent waits on one object. One call stays blocked until another interrupt, a tip change, or its timeout, which can stall a mining integration. The node itself is not affected.

Proposed change

Replace each flag with an interrupt generation. Every wait records the generation when it starts; an interrupt advances it and wakes all waiters, and any wait whose recorded generation is older returns. If no wait is active, one pending interrupt is kept for the next wait, matching today's behavior. createNewBlock keeps one record across its startup, initial block download and cooldown waits.

-                return tip_changed || m_chainman.m_interrupt || interrupt_wait;
+                return tip_changed || m_chainman.m_interrupt || interrupt.interrupted();
             });
-            if (interrupt_wait) {
-                interrupt_wait = false;
-                return nullptr;
-            }
+            if (interrupt.interrupted()) return nullptr;

The report considered a counter consumed once per wait (rejected: one interrupt would still release only one wait) and separate flags for each call (rejected: needs interface changes and does not fit the multi-stage createNewBlock wait). It notes that a request still queued for a worker thread, not yet waiting, can still miss an interrupt.

Assessment

Accurate. On Bitcoin Core master, BlockTemplateManager::InterruptWait() (src/node/block_template_manager.cpp) sets the flag and calls notify_all(), and each waiting function returns when the flag is set and clears it. MinerImpl shares one flag among waitTipChanged, every stage of createNewBlock, and interrupt(); BlockTemplateImpl shares another between waitNext and interruptWait(). Woken waiters take the lock one at a time, so only the first sees the flag. The second wait then lasts until the tip changes or its timeout passes, and waitTipChanged has no timeout by default. Shutdown still releases every wait, so the node can always stop.

Two waits on one object need a client that calls from two threads, which any client can do, including a C++ one. The flags were added with bitcoin#33676 (interruptWait, merged 2025-11-10) and bitcoin#34184 (interrupt, merged 2026-02-24), both in v31.0. The report's test run was not repeated here; the mechanism follows directly from the code.

The proposed fix is correct for the behavior it describes, but it is larger than needed, and the report's branch also carries two unrelated proposed changes (AR-31, AR-38). A plain counter per object, recorded at the start of each wait and never cleared, would release every earlier wait. Whether an interrupt sent while nothing is waiting should cancel the next wait is a separate interface question; the interface documentation says these methods interrupt "the current wait".

Status

Present on master. Next step: a Bitcoin Core PR with a generation counter and the concurrent-wait test, separate from the other two changes. No public issue or PR yet.

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