Audit report AR-68

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 72: Client can use freed memory if the server closes the socket right away
Bug 73: Event loop never exits if a client's setup call fails while it is created

Summary

Two lifetime bugs in how a libmultiprocess client is created, both in Bitcoin Core's copy of the library: if the server disconnects while the client is being set up, the connection can be freed while the client still uses it, and if the client's setup call fails, the connection is never freed. The report is accurate for Bitcoin Core master and says itself that current libmultiprocess already fixed both, in mp#298; Bitcoin Core gets the fix with its next update of the library. Same findings as AR-01 and AR-16. Bug pages: Client can use freed memory if the server closes the socket right away and Event loop never exits if a client's setup call fails while it is created.

The report

ConnectStream() registers a callback that deletes the new Connection when the peer disconnects, and only afterwards constructs the ProxyClient that owns it. That constructor can make an IPC call (construct()) while it uses the connection, so an early disconnect can run the delete callback first and leave the constructor reading freed memory. Separately, if construct() throws, the half-built client's destructor never runs, so its cleanup steps, including deleting the connection, are skipped.

The report rates it low. It needs a Bitcoin Core IPC client, or another libmultiprocess program, whose peer disconnects while the client is being created. It says the reasoning from the code supports both effects, but its runs with AddressSanitizer on the old code did not catch the freed-memory read, and no crash or memory growth in an ordinary build was shown.

Proposed change

The upstream fix adapted to Bitcoin Core's older copy: run the pending cleanup steps if construct() throws, and register the delete-on-disconnect callback only at the end of the client constructor, after construct() has returned:

-    Sub::construct(*this);
+    try {
+        Sub::construct(*this);
+    } catch (...) {
+        CleanupRun(m_context.cleanup_fns);
+        throw;
+    }
+    if (destroy_connection) {
+        m_context.loop->sync([&] {
+            ...
+            connection->onDisconnect([&loop, connection] { ... delete connection; });
+        });
+    }

The tests are the upstream connection tests, including one where the server closes the socket before the client's first call. The report says a public Bitcoin Core change should come from a normal update of the library.

Assessment

Accurate, and already fixed upstream. In Bitcoin Core master's copy ConnectStream still registers the delete callback before the client exists, and the client constructor ends with an unguarded construct() call. mp#298 (merged 2026-08-11) fixed both, after they were reported publicly as mp#308 and mp#309; the report's patch matches it. Bitcoin Core's copy of the library was last updated on 2026-07-14, before that. The report adds no new mechanism or reproduction.

In Bitcoin Core the freed-memory read can be reached by bitcoin-cli, which tries IPC first by default, or the test-only echoipc RPC connecting to a peer that closes at once. The leak needs the server's construct() to fail on a live connection, which Bitcoin Core's server does not do.

Status

Fixed in libmultiprocess master by mp#298; present in Bitcoin Core master until its libmultiprocess copy is updated. Nothing further needed in libmultiprocess.

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