Audit report AR-58

Reported privately in an independent security review, 2026-09-27.

Report date
2026-09-27
Kind
Bug
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 86: Client thread hangs forever if building a request throws

Summary

If anything throws while an IPC client prepares a request, the event loop logs and discards the exception, no request is sent, and the calling thread waits forever for a reply. This is the same bug as AR-14, reported again with a test against the library and a one-line fix. It goes further than the earlier report: it guards all of the request setup, not only building the arguments, and it treats the problem as one case of EventLoop::sync() losing exceptions, which also affects socket setup (AR-57). Bug page: An IPC client thread waits forever in clientInvoke when building the request throws.

The report

clientInvoke runs the request setup inside EventLoop::sync(): it creates the Cap'n Proto request, sets up the call context, serializes the arguments and schedules request.send() with callbacks that will wake the caller. The caller then waits for one of those callbacks to set a completion flag. If any step before the send throws, the event loop logs the exception and returns normally, no callback was registered, and the caller waits forever.

The report lists allocation, conversion, logging and task-registration failures as triggers. Its reproduction is a Bitcoin Core unit test with a real client and server on two event loops: a testing hook throws std::bad_alloc just after the disconnect check in clientInvoke, the test expects the call to throw that error, and then expects the next call on the same client to succeed. The report rates it low: the effect is one blocked client call until restart, with no message from a peer, peer-to-peer path, stored state or funds involved.

Proposed change

Run the setup through a helper that catches the exception on the event loop thread and rethrows it on the calling thread, before it starts waiting:

-    proxy_client.m_context.loop->sync([&]() {
+    SyncWithException(*proxy_client.m_context.loop, [&] {
         if (!proxy_client.m_context.connection) {

SyncWithException() is added by the same series of reports for socket setup (AR-57). The report considered having the event loop's generic catch set the completion flag, which would end the wait but lose the original error and would not help other callers.

Assessment

Accurate, and the same mechanism as AR-14, which was reproduced with a throwing argument serializer on 2026-09-26. The code is unchanged on libmultiprocess master and in Bitcoin Core's copy.

This report is wider in two ways. It guards the whole setup function, which also logs the request and registers the send with the event loop's task set, so a failure in either step is covered too. And its helper is meant for every caller of EventLoop::sync() that expects the work to succeed, not only clientInvoke. Making sync() itself rethrow would cover all of them at once; an open change, mp#376, moves the exception onto the calling thread but still only logs it there. Rethrowing after a failed send registration is safe: the callbacks that refer to the caller's stack are never installed.

In Bitcoin Core, allocation failure terminates the node and GUI instead of throwing, and Bitcoin Core's log callback does not throw, so most of the listed triggers do not apply there. Reading the code, one input can reach it: Cap'n Proto refuses a text field larger than 512 MiB while the request is being built, so a bitcoin-cli request of that size sent over IPC would hang instead of failing. That has not been tried. Smaller oversized messages fail cleanly at send time.

Status

Present on master. Next step: a libmultiprocess issue or PR that propagates the exception, citing this test. No public issue or PR yet.

Reported privately in an independent security review on 2026-09-27.