Audit report AR-80

Reported privately in an independent security review, 2026-10-15.

This page describes the report as it was received. For the current state of the bug, its fix and related PRs and issues, see bug 117.

Report date
2026-10-15
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 117: Durations read from IPC messages are converted without a range check

Summary

libmultiprocess reads a std::chrono::duration parameter by constructing it directly from the number in the message, with no range check. If an interface pairs a Float64 field with an integer duration, a client can send NaN, infinity or a value too large for the integer type, and the conversion is undefined behaviour in C++. An Int64 field read into a narrower integer duration wraps to a wrong value. The report is accurate for the library. Bitcoin Core is not affected: its only IPC duration type holds a double and is sent as Float64, so no conversion takes place. Hardening for other libmultiprocess users.

The report

The generic adapter in include/mp/type-chrono.h passes every wire number straight to the duration constructor. The schema type and the C++ duration's representation can differ, so integer narrowing can wrap, and converting a floating-point value to an integer is undefined for non-finite or out-of-range values. The write side already has compile-time range checks; the read side has none.

The report notes that Bitcoin Core's mining interface uses Float64 for its millisecond timeouts, and that other libmultiprocess interfaces could pair Float64 with an integer or narrower floating-point duration. Its tests add Float64 and Int64 fields paired with a 32-bit integer duration to the test interface, and check that NaN, both infinities and finite values outside the 32-bit range are rejected, and that normal calls still work afterwards. It does not claim a crash in Bitcoin Core's mining interface.

Proposed change

A helper converts the wire value to the duration's representation and throws std::out_of_range when it cannot be represented:

+template <typename Rep, typename Value>
+Rep ChronoRepFromWire(Value value)
+{
+    if constexpr (std::is_floating_point_v<Value>) {
+        if (!std::isfinite(value)) throw std::out_of_range(error);
+        ... // bounds of Rep, for integer and narrower floating-point Rep
+    }
+    if constexpr (std::is_integral_v<Rep> && std::is_integral_v<Value>) {
+        if (!std::in_range<Rep>(value)) throw std::out_of_range(error);
+    }
+    return static_cast<Rep>(value);
+}

CustomReadField uses it before constructing the duration. Values that fit are unchanged.

Assessment

Accurate for libmultiprocess: the read path is read_dest.construct(input.get()), unchecked, on master and in Bitcoin Core's subtree. Not reachable from Bitcoin Core. Its only duration over IPC is MillisecondsDouble, used for the timeouts of waitTipChanged and waitNext, which is a double sent as Float64, so the value arrives unconverted. NaN and negative timeouts are valid doubles and pass through; what Bitcoin Core then does with them is a separate bug, the wait-timeout assertion (AR-38).

The same "check numbers read from the wire against the C++ type" question applies to enums, which are also read with a plain cast (AR-10). A single change validating both would cover the library's numeric adapters.

Status

Present on master. No public issue or PR yet.

Reported privately in an independent security review, 2026-10-15.