Bug 71
IPC code built on NetBSD with GCC before 14 has exception handling compiled out
- Status
- Worked around
- Symptom
- Crash; worst effect seen: crash
- Where seen
- Bitcoin Core functional tests, libmultiprocess tests (mptest), a CI build
- Trigger
- Normal use; no special event is needed
- First reported
- 2025-08-23 (first report)
- Fixed
- 2026-08-03
Summary
On NetBSD with GCC older than 14, Cap'n Proto's kj/common.h mistakes a stub __has_feature macro in <sys/cdefs.h> for "exceptions disabled", so code built there has its exception handling compiled out. With the pkgsrc capnproto package this made bitcoin-node terminate whenever an IPC server method threw (the client sees "Peer disconnected" instead of an error reply), made mptest abort, and, with mixed compilers, made mptest fail to link. The defect is a build-time misdetection in an upstream header; nothing in libmultiprocess or Bitcoin Core code is wrong, and it affects only people building IPC code on NetBSD with an older GCC. libmultiprocess CI works around it in its NetBSD jobs (mp#196, which added them, and mp#321, which moved them to NetBSD 11.0); the upstream fix (capnproto#2756) is not in a capnproto release, and compile-time checks are proposed in mp#364 and bitcoin#36060.
What happened
- Link-error form: the first NetBSD CI job, added by @hebasto in mp#196 (2025-08-23), could not link
mptestagainst pkgsrc capnproto 1.1.0; every error was anundefined reference to kj::getCaughtExceptionAsKj(). ryanofsky tied it toKJ_NO_EXCEPTIONSthe same day; @maflcko posted the full linker error. Building capnproto 1.2.0 from source failed the same way until built with-DKJ_NO_EXCEPTIONS=0, and hebasto traced it to the__has_featuretest inkj/common.hand to capnproto#2756, the upstream fix (2025-12-22). - Runtime form, 2026-07-27, twice the same morning: hebasto's nightly Bitcoin Core run on NetBSD 11.0 with the pkgsrc capnproto package had
interface_ipc.pyandinterface_ipc_mining.pyfail withPeer disconnectedwhere aremote exception: ... Old mining interface (@2) not supportedreply was expected (bitcoin#35823). The NetBSD job of mp#321, which had just dropped mp#196's GCC 14 pin, hadmptestabort in "Destroying ProxyClient<> with destroy method after peer disconnect" withterminate called after throwing an instance of 'std::runtime_error' what(): IPC client method called after disconnect.The two were tied together on 2026-08-01, and bitcoin#35823 was closed as caused by the header bug that capnproto#2756 fixes. - bitcoin#36058 (hebasto, 2026-08-22): the same nightly failure again, filed as an RFC to check exception support at configure time. Its node log shows
mp::serverInvokecatching and rethrowing, then libstdc++'s "terminate called", so the exception is genuinely uncaught above libmultiprocess.
Root cause
kj/common.h tests #if defined(__has_feature) first and, when the macro exists, sets KJ_NO_EXCEPTIONS if !__has_feature(cxx_exceptions); only a compiler without the macro falls through to the __GNUC__/__EXCEPTIONS test (capnproto 1.2.0 and 1.4.0). NetBSD's <sys/cdefs.h> defines a dummy __has_feature(x) 0 for compilers that lack it natively, which is GCC older than 14. So every translation unit that GCC 10 or 12 compiles with kj/common.h on NetBSD is built as if exceptions were disabled.
- Mixed compilers (mp#196): libmultiprocess built with GCC 14, whose native
__has_feature(cxx_exceptions)is true, against alibkjbuilt with GCC 10 and the stub. The headers referencekj::getCaughtExceptionAsKj(), which that library omits, so linking fails. - Same compiler on both sides:
mp::serverInvoke(include/mp/proxy-types.h) catches a server method's exception, logs it, and rethrows for Cap'n Proto's RPC layer to turn into a remote-exception reply. In alibcapnp-rpcbuilt withKJ_NO_EXCEPTIONSthat catch is compiled out andbitcoin-nodeterminates. On the client side, thekj::runCatchingExceptionsaround the destructor-timedestroy()call inProxyClientBaseis compiled with the same misdetection inmptest, so thestd::runtime_errorescapes andmptestaborts.
Fix
No fix in libmultiprocess or Bitcoin Core code; the upstream fix, capnproto#2756, is not yet in a capnproto release or in NetBSD's pkgsrc package. libmultiprocess CI works around it:
- 861da39cae99 (hebasto, mp#196) builds capnproto from source with
CPPFLAGS="-DKJ_NO_EXCEPTIONS=0"and pins GCC 14 for libmultiprocess. - 2d33b14fb0e7 (hebasto, mp#321) switches the NetBSD jobs to the default compiler and adds
CXXFLAGS="-DKJ_NO_EXCEPTIONS=0"for the libmultiprocess build, with a comment naming the upstream bug.
Open proposals: mp#364 (ryanofsky) adds an #error so a build with KJ_NO_EXCEPTIONS set fails at compile time instead of crashing at runtime, and bitcoin#36060 (@151henry151, draft) adds a configure-time check.
Timeline
- 2025-08-23: link errors in mp#196.
- 2025-12-22:
__has_featuremisdetection identified by @hebasto. - 2026-01-09: first CI workaround merged in mp#196.
- 2026-07-27: runtime form in bitcoin#35823 and in mp#321 CI.
- 2026-08-03: second CI workaround merged in mp#321.
- 2026-08-22: bitcoin#36058.
- 2026-09-10: mp#364 opened; not merged.
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 feature macro detected wrongly
- Prevented by nothing practical
- Earliest detector an assertion (did not exist then)
- Diagnosis hard
- Fix design easy
- Debugging aids logging, CI artifacts
- Rust n/a
Would Rust have prevented it? Not applicable. A feature macro misdetected by a header on NetBSD is a build and toolchain problem in which no libmultiprocess code is wrong. More on Rust
Could it have been prevented?
Not by this project. The defect is in Cap'n Proto's kj/common.h, which trusts a
__has_feature(cxx_exceptions) answer of zero without checking that the macro is the
compiler's own; NetBSD's <sys/cdefs.h> stubs __has_feature to 0 for GCC older than
14, so every unit built there has exception handling compiled out. Nothing in
libmultiprocess or Bitcoin Core code is wrong, and the upstream fix
(capnproto#2756) is not in a
release. What the project can do is refuse to build in that state: libmultiprocess
depends on serverInvoke rethrowing into the RPC layer and on
kj::runCatchingExceptions actually catching, so an #error when KJ_NO_EXCEPTIONS
is set, which mp#364
proposes, turns a runtime abort on every error path into a build failure with a
message. That assumption ("the library is built with exceptions") was never written
down because nobody imagined a build where it was false.
Could it have been found sooner?
The link-error form was found the first day anyone built on NetBSD
(mp#196, 2025-08-23) and
ryanofsky named KJ_NO_EXCEPTIONS
the same day, so detection was not the problem; the runtime form was hidden for eleven
months by the workaround itself, since pinning GCC 14 for libmultiprocess and building
capnproto with -DKJ_NO_EXCEPTIONS=0 removed the mismatch from the only NetBSD job.
The runtime form surfaced twice on the morning of 2026-07-27, in hebasto's nightly
Bitcoin Core run against the pkgsrc package
(bitcoin#35823) and in the
mp#321 job
once it dropped the pin. A compile-time check would have caught both forms at
configure time in 2025; a CI job that builds the library the way a distribution
package does (default compiler, prebuilt capnproto) is what actually did.
What made it hard to debug?
The two forms looked unrelated: undefined references to kj::getCaughtExceptionAsKj()
in 2025, then "Peer disconnected" where a remote-exception reply was expected and a
terminate called after throwing abort in 2026, and they were only
tied together
on 2026-08-01. hebasto's
December diagnosis
of the header logic was correct but its premise was questioned as late as September
2026, because pkgsrc passes no -fno-exceptions and nothing visible in the build
explained why exceptions would be off; the <sys/cdefs.h> stub that completes the
story was found by reading NetBSD's system header. The node log in
bitcoin#36058, showing
mp::serverInvoke catch and rethrow followed by libstdc++'s terminate message, was
what proved the exception escaped above the library rather than being mishandled in
it. A preprocessor-level diagnostic (#error or a configure check printing the
detected exception mode) would have replaced all of that with one line.
Lesson
When a library's correctness depends on a build-configuration assumption such as "exceptions are enabled", assert it at compile time; a feature-detection macro can be wrong on a platform you have not built on, and the runtime symptom of a silently disabled catch block is a crash far from the cause.
Notes
- Nobody built IPC code on NetBSD before hebasto's 2025-08 CI work. mp#196's workaround and GCC 14 pin then hid the runtime form from libmultiprocess CI until mp#321 switched to the default compiler. hebasto's Bitcoin Core nightly builds with NetBSD 11.0's default GCC 12.5 against the prebuilt package rather than through depends, which is why it hit the runtime form.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | crash |
| Worst effect observed | crash |
| Seen in | functional-test, mptest, ci-build |
| Scope | build |
| Trigger | Normal use (ordinary-path) |
| Phase | any |
| Side | both |
| Component | capnproto |
| Root cause kind | unhandled-exception |
| Fix kind | config |
| Detection | ci-deterministic |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Confidence | medium |