Bug 43
IPC build hangs forever with Cap'n Proto 0.8 in C++20 mode
- Status
- Fixed
- Symptom
- Hang; worst effect seen: hang
- Where seen
- A CI build
- Trigger
- Normal use; no special event is needed
- First reported
- 2025-08-12 (first report)
- Fixed
- 2025-08-22
- Fix
- mp#194
- Shipped to Bitcoin Core
- bitcoin#33241
Summary
Building Bitcoin Core with IPC enabled against Cap'n Proto 0.8 (for example Ubuntu 22.04's libcapnp-dev) never finished, because the mpgen code generator hung in an infinite loop. The cause was a const char*/StringPtr comparison operator in Cap'n Proto 0.8's kj/string.h that calls itself when compiled as C++20. This was a build-time problem only; programs that did get built were not affected. Worked around in mp#194, which also added a CI job building against old dependencies, and shipped to Bitcoin Core before v30.0.
What happened
- @fanquake, looking at bitcoin#31802 (which added
bitcoin-nodeandbitcoin-guito the release binaries), found that building master with-DENABLE_IPC=ONin anubuntu:22.04container with the distrolibcapnp-dev0.8.0 hung forever at theGenerating capnp/*.capnp.c++steps that runmpgen(bitcoin#33176, "build: indefinite mpgen hang on Ubuntu 22.04", 2025-08-12). - @willcl-ark hit the same hang the same day while building his bitcoin-core-docker image (willcl-ark/bitcoin-core-docker#43).
- ryanofsky attached gdb and found
mpgenspinning on ajmpinstruction that jumps to itself insidemain, then found the cause and a workaround.
Root cause
kj/string.h in Cap'n Proto 0.8 defines
inline bool operator==(const char* a, const StringPtr& b) { return b == a; }
Before C++20 the body converts a to StringPtr and calls the member StringPtr::operator==. C++20 also considers comparison operators with their arguments reversed, and with those candidates the call instead resolves to this same free function, so it recurses forever. The optimizer turns the infinite tail recursion into a self-loop. Generate() in src/mp/gen.cpp compared kj::StringPtr values against string literals (src_prefix == ".", method_name == "construct" and others), which hit this overload. Cap'n Proto fixed it upstream in capnproto#1170, released in 0.9.0.
Bitcoin Core has required C++20 since v27.0, but libmultiprocess's CI and Bitcoin Core's depends build used newer Cap'n Proto versions, so nothing built mpgen against 0.8 with C++20 until this report.
Fix
The workaround commit c4cb758eccb5 wraps the five string literals compared in Generate() as kj::StringPtr{...}, so the member operator is chosen (+5/-5). The same PR adds an olddeps CI job that builds against Cap'n Proto 0.8 (8ceeaa6ae401) and a check for Cap'n Proto versions affected by CVE-2022-46149 (dc3ba2204606).
Timeline
- 2025-08-12: reported by @fanquake in bitcoin#33176 and confirmed by @willcl-ark.
- 2025-08-22: workaround merged in mp#194.
- 2025-08-25: shipped to Bitcoin Core master in bitcoin#33241 ("Update libmultiprocess subtree to fix build issues"), before v30.0.
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 CI, toolchain and platform: A minimum dependency version CI never built
- Prevented by nothing practical
- Earliest detector other (did not exist then)
- Diagnosis moderate
- Fix design easy
- Debugging aids symbolized stack traces, a reproduction script
- Rust n/a
Would Rust have prevented it? Not applicable. A Cap'n Proto header that recurses under C++20 comparison rewriting is a toolchain and dependency problem; no libmultiprocess code is at fault and the question does not apply. More on Rust
Could it have been prevented?
Not practically. The defect is in Cap'n Proto 0.8's kj/string.h, whose
operator==(const char*, const StringPtr&) calls itself once C++20's reversed
comparison candidates are in play; upstream fixed it in
capnproto#1170 for 0.9.0. Nothing
in mpgen's five StringPtr == "literal" comparisons was wrong by the library's own
rules, and the workaround
(c4cb758eccb5)
only spells the literals as kj::StringPtr{...} so the member operator wins. The
knowable part is policy: the project declared 0.8 supported (it is Ubuntu 22.04's
package) while Bitcoin Core had required C++20 since v27.0, and no build ever combined
the two.
Could it have been found sooner?
Yes, by a CI job that builds against the oldest dependency versions the project claims
to support, which is what mp#194
added as the olddeps job
(8ceeaa6ae401).
Both libmultiprocess CI and Bitcoin Core's depends build used newer Cap'n Proto, so the
first C++20 build of mpgen against 0.8 was fanquake's container while checking
bitcoin#31802, days before the first
release to ship IPC binaries. The hang is deterministic, so that job would have failed on
its first run at any point after the C++20 switch.
What made it hard to debug?
The symptom was a build step that never finished with no output, in a container the
reporter had to describe. ryanofsky
attached gdb
and found mpgen on a jmp to itself inside main, which says "infinite loop" but not
where, because the optimizer had folded the tail recursion into a single instruction; the
cause came
from reading the kj header with C++20's rewritten-candidate rules in mind, the same day.
An unoptimized build would have shown a stack overflow with the operator's name on every
frame, and a compiler warning for a self-recursive rewritten candidate would have been
the ideal aid, but none exists.
Lesson
A supported minimum dependency version that CI never builds is a claim, not a fact; build against the floor as well as the ceiling, especially after raising the language standard, since a newer standard can change what old library code means.
Notes
- ryanofsky later suggested that the recursion might be caused by GCC bug 53499, fixed in GCC 14 (Ubuntu 22.04 uses GCC 11.4), in which case building with clang would be another workaround. This was not investigated further.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | hang |
| Worst effect observed | hang |
| Seen in | ci-build |
| Scope | build |
| Trigger | Normal use (ordinary-path) |
| Phase | any |
| Side | n/a |
| Component | capnproto |
| Root cause kind | other |
| Fix kind | workaround |
| Detection | user-report |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Confidence | low |