Audit report AR-60
Reported privately in an independent security review, 2026-09-27.
- Report date
- 2026-09-27
- Kind
- Hardening
- Status on master
- Present
- Proposed issue state
- Open
Summary
Before libmultiprocess runs an asynchronous IPC method on a worker thread, it installs a callback that protects the request if the client cancels or disconnects while the method runs. The report shows that if installing that callback threw, the event loop would drop the error and the method would run anyway, without the protection. The mechanism is accurate, but the only possible failure there is a memory allocation, and the Bitcoin Core node and GUI terminate on allocation failure. Robustness hardening, not a bug that can be hit today.
The report
For a method with a Context argument, the server prepares the call on the event loop thread inside EventLoop::sync() before running it on a worker thread. One step stores a callback in CancelMonitor::m_on_cancel: if the request is canceled while the method runs, the callback blocks the event loop until the worker stops using the request's parameters and results. Assigning that callback to a std::function can allocate memory and throw. The event loop logs and drops the exception, and the worker then runs the method without the callback and reports success.
The reproduction is a Bitcoin Core unit test whose testing hook throws std::bad_alloc just before the assignment. Without the fix, the method ran and returned success. A variant that also disconnected the client did not produce a sanitizer report. The report rates it low: it needs a local allocation failure, and no remote trigger, stored state or funds are involved.
Proposed change
Catch the exception inside the event loop function, skip the rest of the setup, and rethrow it on the worker thread after the existing cleanup has been set up, so the method does not run and the client receives the error:
+ std::exception_ptr cancel_setup_exception;
loop.sync([&] {
...
- cancel_monitor.m_on_cancel = [&loop, &server_context, &cancel_mutex, req]() {
+ try {
+ cancel_monitor.m_on_cancel = [&loop, &server_context, &cancel_mutex, req]() {
...
+ } catch (...) {
+ cancel_setup_exception = std::current_exception();
+ return;
+ }
...
+ if (cancel_setup_exception) std::rethrow_exception(cancel_setup_exception);
The test then checks that the next call on the same connection succeeds. The report notes that a later step in the same setup function, registering the request's thread, has its own failure case, which AR-34 covers.
Assessment
The mechanism is accurate on libmultiprocess master and in Bitcoin Core's copy. If the assignment threw, the rest of the setup would be skipped as well, and a disconnect while the method ran could then free the request's data while the worker was still reading it. Without a disconnect, the call just succeeds, as the report says.
In practice the assignment can fail only by running out of memory. The callback captures more data than std::function stores inline, so it is allocated on the heap, and nothing else in the assignment can throw. Bitcoin Core installs an out-of-memory handler at startup that terminates the node and GUI instead of throwing, so there the result is the same with or without the fix. Other applications built on libmultiprocess would get an error instead of an unprotected call.
The proposed change looks correct, though it was not run here. The same pattern, an exception thrown inside EventLoop::sync() being logged and dropped, also appears in AR-57 and AR-58, where it can be reached. A change that makes sync() rethrow on the calling thread would also cover this case, which may be simpler than a dedicated catch here.
Status
Present on master. Hardening; no public issue or PR yet.
Reported privately in an independent security review on 2026-09-27.