Audit report AR-38

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

Report date
2026-09-18
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 84: Negative or NaN wait timeout from a mining client aborts debug builds of the node

Summary

Mining.waitTipChanged takes a Float64 timeout from the IPC client and checks it with Assume(timeout >= 0ms) before clamping it. A negative timeout aborts bitcoin-node in Debug and fuzz builds, and a NaN timeout passes every range check and reaches the clock arithmetic, which is undefined behaviour in any build. The report is correct and reproduced the abort against a real node. Real bug, open; the fix is small and mergeable.

The report

The mining IPC schema declares waitTipChanged @3 (context :Proxy.Context, currentTip: Data, timeout: Float64 = .maxDouble) with no restriction on the timeout. The node's WaitTipChanged treats the value as an internal precondition:

Assume(timeout >= 0ms); // No internal callers should use a negative timeout
if (timeout < 0ms) timeout = 0ms;
if (timeout > std::chrono::years{100}) timeout = std::chrono::years{100};
auto deadline{std::chrono::steady_clock::now() + timeout};

A negative timeout therefore aborts builds where a failed Assume aborts (Debug, fuzz, and others built with ABORT_ON_FAILED_ASSUME), and the client is disconnected. In ordinary release builds Assume does nothing and a negative value is clamped to zero. NaN, however, fails both comparisons, skips both clamps, and reaches the deadline computation and the condition variable's wait_until, where converting NaN to an integer tick count is undefined behaviour.

The report states that one request from a client with access to the mining interface terminates a checked build, that restarting recovers, and that there is no P2P or RPC path and no effect on blocks, mining policy or consensus. It did not show a release-build failure for NaN.

The reproduction is a case added to the functional test test/functional/interface_ipc_mining.py, which calls waitTipChanged over IPC with -1.0 and NaN and expects both to return the current tip immediately. Without the fix, the negative value hit the Assume and disconnected the client. For NaN it adds a separate standalone probe of the same libc++ wait_until conversion, which UBSan reports as an out-of-range NaN to long long conversion; that probe does not use the node.

Proposed change

Treat negative and NaN timeouts as zero before computing the deadline, and drop the Assume:

-    Assume(timeout >= 0ms); // No internal callers should use a negative timeout
-    if (timeout < 0ms) timeout = 0ms;
+    if (std::isnan(timeout.count()) || timeout < 0ms) timeout = 0ms;
     if (timeout > std::chrono::years{100}) timeout = std::chrono::years{100}; // Upper bound to avoid UB in std::chrono

Positive values keep the existing 100-year cap.

Assessment

Present on Bitcoin Core master, where bitcoin#35675 moved the function unchanged into BlockTemplateManager::WaitTipChanged; the diff targets the old location in src/node/miner.cpp and needs a trivial rebase. The timeout goes straight from the schema's Float64 into MillisecondsDouble.

No malicious client is needed. A client computing "deadline minus now" can easily pass a small negative value after its own deadline has passed, and the C++ interface accepts any double. In release builds a negative value is harmless and NaN most likely returns immediately, but NaN reaches undefined behaviour, so the outcome is not guaranteed. This is the same class of problem as bitcoin-node aborts on Assert when an IPC client calls createNewBlock with out-of-range or default BlockCreateOptions, fixed by bitcoin#33222: an assertion about internal callers that an IPC client can violate.

Bug page: bitcoin-node aborts on Assume (Debug and fuzz builds) or reaches undefined behaviour (NaN) when an IPC client passes a negative or NaN waitTipChanged timeout.

Reproduction (2026-09-26). A separate functional test against a Debug node built from Bitcoin Core master bfdcd97 called waitTipChanged with a timeout of -1.0 over IPC. The node aborted on Assertion 'timeout >= 0ms' failed and the client was disconnected, confirming the report. NaN was not tried. See the bug page's reproduction section.

Status

Open on Bitcoin Core master. Next step: rebase the fix and its functional test onto BlockTemplateManager and open it as a Bitcoin Core PR. No public PR yet.

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