Bug 48
32-bit x86 IPC server crashes while logging an exception thrown by a server method
- Status
- Open
- Symptom
- Crash; worst effect seen: crash
- Where seen
- Libmultiprocess tests (mptest), Bitcoin Core functional tests
- Trigger
- Unusual input from a peer or caller
- First reported
- 2025-10-03 (first report)
- Fixed
- Not yet
- In released binaries
- No; the code was in source releases only
- Defect ever on master
- Unknown
Summary
On 32-bit x86 builds, an IPC server process crashed with a segfault whenever a server method threw an exception and the server tried to format it for a log message with kj::str(e). The crash is inside kj's out-of-line kj::operator*(kj::_::Stringifier, const kj::Exception&) formatting function, which receives a reference that points to garbage instead of the exception. It was seen in libmultiprocess's gnu32 CI job and in a Debian i386 sanitizer build of bitcoin-node. The cause is not known. There are no official 32-bit x86 release binaries, so only people who build IPC for that architecture themselves are affected. There is no fix. Of the two places that format an exception this way, one now logs the exception differently and avoids the crash; the other still crashes.
What happened
- mp#218 ("Better error and log messages") added a
kj::str("kj::Exception: ", e)log line toserverInvokeininclude/mp/proxy-types.h, the library function that runs an incoming call on the server. The libmultiprocess gnu32 CI job (a nix cross build fori686-unknown-linux-gnu) then segfaulted whenever a server method threw: first in the "thread busy" test, and, after the PR was rebased on mp#240, in the "disconnecting and blocking" test (first failure, 2025-10-03). ryanofsky reproduced it locally and traced it in gdb the same day. - @maflcko filed bitcoin#34732 (2026-03-04) from an ASan job on Debian trixie
linux/i386that used the distribution'slibcapnppackage. Ininterface_ipc_mining.py, the step that overrides mining options madecreateNewBlockthrow "block_reserved_weight (0) must be at least 2000 weight units". The worker thread running the call then formatted the exception withkj::str(*exception)inPassField(include/mp/type-context.h) and died withAddressSanitizer: SEGV on unknown address ... in kj::operator*(kj::_::Stringifier, kj::Exception const&) (/lib/i386-linux-gnu/libkj-1.1.0.so+0x38ce0). ryanofsky called it "very similar" to the gnu32 failures. maflcko closed the issue on 2026-06-12 and moved it to his nightly CI tracker.
In both cases the server process is gone, and the client sees "Peer disconnected" instead of the text of the remote exception.
Root cause
Not established. In gdb on the gnu32 build, the kj::Exception&& e in serverInvoke is a valid object with the expected description, but two frames deeper, in kj's formatting function, the reference argument points to a different address holding garbage (gdb comment; review thread, where ryanofsky suspected an ABI issue or compiler bug). Holding an extra capability reference, using catch_ instead of then, and explicit lambda return types did not help. Only replacing kj::str(e) with e.getDescription(), which returns the exception's message without calling the formatting function, avoided the crash. maflcko described the Debian case as "another one of the GCC+clang mix things".
A possible explanation, not confirmed by anyone who worked on it: kj::_::Stringifier is an empty struct passed by value as the first parameter of the out-of-line formatting function. That is the same shape as the i686 calling-convention mismatch between GCC and clang for empty classes that broke kj's placement operator new (bitcoin#31772, a 32-bit clang ipc_test failure; see that bug's page). It would fit the Debian build, where a clang-built bitcoin-node calls a GCC-built libkj. It does not explain the gnu32 sighting, where Cap'n Proto and libmultiprocess are both built with GCC, so the cause remains open.
The crash only happens once server code formats a caught exception. libmultiprocess started doing that with mp#218's log line. Bitcoin Core started doing it with the "uncaught exception" log line in PassField, added by 0174450ca2e9 in mp#240 ("Avoid errors from asynchronous (non-c++) clients"). Bitcoin Core's own CI stopped building IPC for i686 in 2025-08, when bitcoin#31802 (which added bitcoin-node and bitcoin-gui to the release binaries) turned the "multiprocess, i686" job into a job without IPC. Since then only libmultiprocess's gnu32 job and custom nightly configurations exercise 32-bit x86 IPC.
Fix
No fix yet. mp#218 was merged (2026-06-02) with its log line using e.getDescription() in serverInvoke (f326c5b1b7b4). That avoids the crash at that site but logs less than kj::str(e) would. It reached Bitcoin Core in bitcoin#35661 (2026-07-07), a subtree update. The kj::str(*exception) call in PassField is still on master, so an affected 32-bit build still crashes whenever a server method throws.
Next steps
- Open the drafted libmultiprocess issue (an issue to be opened). bitcoin#34732 was closed without a fix and moved to a nightly CI tracker, so no open issue tracks the crash.
- Choose one of two paths. One is to find the cause with a small standalone program that formats a
kj::Exceptionwithkj::strusing the same i686 compilers and libraries; one lead is the guess that this is the same GCC and clang empty-class ABI mismatch as in bitcoin#31772, though that would not explain the all-GCC gnu32 sighting. The other is to loge.getDescription()at the remaining call sites, as mp#218 did inserverInvoke, which logs less detail. - The remaining call sites are
PassFieldininclude/mp/type-context.h(the one that crashed in bitcoin#34732),EventLoop::loop()insrc/mp/proxy.cpp, andclientInvokeininclude/mp/proxy-types.h. After a library fix is merged, update Bitcoin Core's libmultiprocess subtree.
Timeline
- 2025-10-03: gnu32 CI segfaults on mp#218; ryanofsky diagnoses it in gdb.
- 2026-02-20: mp#240 merged, adding the
PassFieldlog line to libmultiprocess. - 2026-03-03: the
PassFieldlog line reaches Bitcoin Core in bitcoin#34422, the subtree update that brought in mp#240. - 2026-03-04: bitcoin#34732 filed by maflcko.
- 2026-06-02: mp#218 merged with the
getDescription()workaround; 2026-06-12: bitcoin#34732 closed without a fix. - Releases: no release binaries affected; 32-bit x86 builds from source with IPC enabled are.
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: Objects from two compilers in one program
- Prevented by nothing practical
- Earliest detector a unit test (existed then)
- Diagnosis hard
- Fix design hard
- Debugging aids symbolized stack traces, core dumps, other
- Rust n/a
Would Rust have prevented it? Not applicable. An undiagnosed crash inside kj's exception formatter on 32-bit x86, probably a compiler or ABI issue, is a toolchain problem below the library's control; a capnproto-rust build would not contain kj at all, but the question does not apply to the C++ code. More on Rust
Could it have been prevented?
Not practically. The crash is inside kj's exception formatter on 32-bit x86, reached by
an ordinary kj::str(e) on a caught kj::Exception, and the cause has not been found:
gdb shows a valid exception in serverInvoke and a garbage reference two frames down.
The suspected explanations (an i686 calling-convention gap between GCC and clang for
the empty Stringifier argument, of the kind that broke kj's placement operator new
in bitcoin#31772, or a compiler bug)
are below the library's control, and the all-GCC gnu32 sighting does not fit the first
one. No rule about how libmultiprocess is written would have avoided calling a
documented kj function on a kj exception. The only choice available was the one
mp#218 made: log e.getDescription() instead, which loses the exception type and
location. The pattern label groups it with the placement-new ABI bug on the strength
of the suspected mechanism and maflcko's "GCC+clang mix" reading; it is provisional
because the cause is.
Could it have been found sooner?
It was found as early as it could be: the mptest unit test in which a server method
throws, run by the libmultiprocess gnu32 CI job, failed the day
mp#218 added the first
server-side kj::str(e) (2025-10-03), and maflcko's nightly i386 ASan job failed the
day after mp#240's
PassField log line reached Bitcoin Core (2026-03-04). What is missing is not a
detector but coverage: Bitcoin Core's own CI stopped building IPC on i686 in
2025-08, so the second site sat on master for a year, and a server method throwing on
an affected 32-bit build still crashes the node. A 32-bit job that runs a test where a
server method throws is the check, and it exists only outside the main repository.
What made it hard to debug?
The cause is still unknown after two gdb sessions and several tried workarounds (an
extra capability reference, catch_ instead of then, explicit lambda return
types). Nothing symbolizes a corrupted reference inside a distribution libkj.so;
ASan reported only "SEGV on unknown address"; the third possible sighting
(bitcoin#34014) had an unsymbolized
stack, so it could not be tied to the same function. What would help is what the ABI
diagnosis of bitcoin#31772 needed: a minimal program calling the formatter from the
same two compilers, disassembly of both sides of the call, and a build of kj with
debug info on the affected platform. Symbolized traces from the nightly jobs and a
core dump would at least let the sightings be confirmed as one bug.
Lesson
When a crash lives in a dependency on one platform and only under one mix of compilers, a workaround that avoids the call is a stopgap, not a fix; keep at least one CI job on that platform exercising the error path, so the second site does not sit unnoticed for a year.
Notes
- An earlier i686
mptestsegfault in the "thread busy" test (bitcoin#34014, 2025-12-05, with an unsymbolized stack) may be a third sighting. maflcko later pointed to bitcoin#34732 for it, but no stack trace ties it to this function.
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 | mptest, functional-test |
| Scope | production |
| Trigger | Unusual input (unusual-input) |
| Phase | steady-state |
| Side | server |
| Component | capnproto |
| Root cause kind | unknown |
| Fix kind | none |
| Detection | ci-intermittent |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | unknown |
| Released | source |
| Confidence | medium |