Bug 117

Durations read from IPC messages are converted without a range check

Status
Open
Symptom
Undefined behavior; worst effect seen: nothing observed
Where seen
Never observed
Trigger
Unusual input from a peer or caller
Introduced
33c2eee285e3, 2024-07-17
First reported
Reported privately in an independent security review (audit report AR-80), 2026-10-15
Fixed
Not yet
Tracked in
Not tracked on GitHub yet
In released binaries
Yes
Defect ever on master
Yes

Summary

libmultiprocess reads a std::chrono::duration by constructing it straight from the number in the message. If an application maps a floating-point schema field to a duration with an integer count, a NaN, infinite or too-large value from the peer is converted to an integer anyway, which is undefined behaviour in C++; an integer field read into a narrower integer wraps around. Bitcoin Core's only IPC duration is a double read from a floating-point field, so nothing is converted and Bitcoin Core is not affected. It was reported privately in an independent security review (audit report AR-80). No fix yet.

What happened

Nothing has been seen in use. An independent security review reported it on 2026-10-15 (audit report AR-80, which gives the report as received), with tests that read floating-point and 64-bit integer values into a 32-bit duration. The report says no crash was demonstrated against Bitcoin Core.

Root cause

When a duration is written, CustomBuildField uses static_assert to make sure the schema field can hold every value of the C++ type. Reading has no matching check: CustomReadField passes input.get() to the duration's constructor. When the schema field is Float64 and the duration counts in an integer type, C++ converts the double to that integer, and converting NaN, infinity or a value outside the integer's range is undefined behaviour. When the field is Int64 and the count is narrower, the value is silently reduced modulo the type's range. The time_point overload reads the same way.

Bitcoin Core declares one IPC duration, MillisecondsDouble (the timeouts of waitTipChanged and waitNext), carried as Float64 into a double, so no conversion happens. NaN and negative values that a double does carry are a separate problem in Bitcoin Core's mining code, recorded as the wait-timeout bug.

Origin: the read function was written in Bitcoin Core's src/ipc/capnp/common-types.h as part of the mining IPC wrapper, 33c2eee285e3 (bitcoin#30510, merged 2024-09-25, first released in v29.0), and moved into libmultiprocess unchanged by 798f4b5e3578 (mp#135, 2025-01-27). mp#303 (2026-08-03) added the time_point overload with the same shape.

Fix

No fix yet. The report adds a helper that rejects non-finite floating-point values and values outside the duration's range, throwing std::out_of_range, which libmultiprocess returns to the caller as an error. It would fit with an optional check on enum values as one "validate numbers read from the wire" change.

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 (did not exist then)
  • Diagnosis easy
  • Fix design easy
  • Rust yes

Would Rust have prevented it? Yes. Rust float-to-int as casts saturate (NaN becomes 0) since 1.45, so there is no undefined behaviour, only a wrong value; TryFrom makes the range check explicit. More on Rust

Could it have been prevented?

Yes. The build side already checks ranges at compile time; the read side needed the same thought at run time, because a value read from another process is input, not a value this process produced.

Could it have been found sooner?

Yes, by a review question on the read path or a fuzz target over a schema with mixed field and C++ types. Bitcoin Core's own schemas never mix them, so its tests could not.

What made it hard to debug?

Nothing has happened yet; undefined behaviour from a float conversion would show as a garbage value, not a crash, and be hard to trace back.

Lesson

Compile-time range checks on what you send do not protect what you receive.

Classification

The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.

Field Value
Symptom ub
Worst effect observed none
Seen in never
Scope production
Trigger Unusual input (unusual-input)
Phase steady-state
Side both
Component proxy-types
Root cause kind assumption-violated
Fix kind none
Detection review
Needs a peer that is not libmultiprocess C++ no
Needed a sanitizer to see no
Defect ever on master yes
Released binary
Confidence high