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
- Low priority: no Bitcoin Core field is affected. Consider the range check if libmultiprocess adds input validation for other number types.
Timeline
- 2024-09-25: bitcoin#30510 merged, adding the duration read function in Bitcoin Core.
- 2025-01-27: moved into libmultiprocess by mp#135.
- 2026-08-03: mp#303 adds
time_pointsupport with the same read shape. - 2026-10-15: reported privately in an independent security review (AR-80).
- Releases: the code is in v29.0 and later; no Bitcoin Core field reaches it.
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 |