Audit report AR-49

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

Report date
2026-09-27
Kind
Bug
Status on master
Present
Proposed issue state
Closed
Linked bugs
Bug 81: Node crashes when a client calls a method on an object it already destroyed
Bug 82: Destroying an object during a running call frees it under the call

Summary

This report describes the same two problems as AR-17: calling a method on an object after the client destroyed it makes the server call through a null pointer, and destroying an object from a second worker thread while one of its methods is running frees it under that call. Its author withdrew it the same hour in favour of AR-17 and moved its tests there. Both bugs are open on master and tracked in libmultiprocess#378. Bug pages: Node crashes when a client calls a method on an object it already destroyed and Destroying an object during a running call frees it under the call.

The report

An IPC server should keep an object's implementation alive while one of its methods runs and reject calls made after the client destroys it. Instead, the destroy handler resets the only owning pointer, and ordinary method dispatch calls through a raw pointer without checking or holding it. A call after destroy therefore goes through a null pointer, and destroy run on another worker thread while BlockTemplate.waitNext() is waiting can free the object under that call.

The report says the first case crashed a release-build node with a segmentation fault, the second produced a heap-use-after-free under AddressSanitizer, and a fix that only copied the owning pointer without a lock produced a data race under ThreadSanitizer. It notes that these sanitizer runs were not repeated on the final branch. It rates the problem low: the caller needs a local IPC connection, and no peer-to-peer or RPC trigger, lasting damage or funds effect was shown.

Proposed change

Guard the owning pointer with a mutex, have each call take a counted copy under that mutex and hold it for the duration of the method, and return an error for calls on an already destroyed object:

-        return (server_context.proxy_server.m_impl.get()->*ProxyMethod<MethodParams>::impl)(std::forward<Args>(args)...);
+        auto impl{server_context.proxy_server.GetImpl()};
+        if (!impl) {
+            throw std::runtime_error{"called method on destroyed object (destroy() already called)"};
+        }
+        return (impl.get()->*ProxyMethod<MethodParams>::impl)(std::forward<Args>(args)...);

Two functional tests in interface_ipc.py call Echo and BlockTemplate methods after destroy and expect an error with the connection still usable, and destroy a block template from a second worker thread while waitNext() is waiting on the first.

Assessment

The report is accurate and matches AR-17; the code on libmultiprocess master is unchanged since that assessment. The fix differs from AR-17's in one respect: here destroy succeeds during a call and the object is kept alive only until the method returns, while AR-17's version also covers the time spent sending the results back and refuses destroy while calls are active. Neither version covers server methods written by hand that use the implementation pointer directly, which is why the fix discussed in libmultiprocess#378 places the check where every request passes.

What this report adds is a pair of tests that go through real Bitcoin Core interfaces, including the waitNext() race, where the earlier reproductions used a libmultiprocess test with a hook to force the timing. Its results were not repeated here. The call-after-destroy crash was later reported publicly in bitcoin#36502, found independently by fuzzing.

Status

Withdrawn as a duplicate of AR-17. Both bugs are present on master and tracked in libmultiprocess#378.

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