Audit report AR-01

Reported privately in an independent security review, 2026-08-07.

Report date
2026-08-07
Kind
Bug
Status on master
Fixed in mp#298
Proposed issue state
Closed
Linked bugs
Bug 72: Client can use freed memory if the server closes the socket right away

Summary

A server that closes a new connection right away can make the connecting client free its Connection object before the ProxyClient that should own it exists, a use-after-free in the client process. The report restates a bug that was already public as mp#308. Real bug, fixed in libmultiprocess by mp#298 four days after the report; the fix has not yet reached Bitcoin Core's copy of the library.

The report

ConnectStream in include/mp/proxy-io.h (as of libmultiprocess commit 67302cd, 2026-08-03) created the Connection inside loop.sync(), fetched the bootstrap capability, and registered this disconnect handler:

Connection* connection_ptr = connection.get();
connection->onDisconnect([&loop, connection_ptr] {
    delete connection_ptr;
});

After sync() returned, the calling thread constructed ProxyClient<InitInterface> and only then handed over ownership with connection.release(). The report describes the failure sequence:

  1. The server accepts and immediately closes the socket.
  2. On the event-loop thread, the disconnect handler runs and deletes the Connection, including its EventLoopRef.
  3. The calling thread, still holding the raw pointer in its local unique_ptr, constructs the ProxyContext from it.
  4. ProxyContext reads connection->m_loop from freed memory.

It cites the real NetBSD CI abort from mp#308 and notes that the race exists on every platform. Its impact statement: a server that closes at the right moment corrupts memory in the connecting process, and a crash is the demonstrated outcome.

The reproduction is a standalone C++ program that models the ordering with plain new/delete and a pair of promises, not a test against the library. The event-loop thread deletes the object, then the main thread builds a ProxyContext from the stale pointer; AddressSanitizer reports heap-use-after-free ... in ProxyContext::ProxyContext(Connection*).

Proposed change

Keep a single stable owner of the Connection until the proxy is fully constructed, and register the delete-on-disconnect behaviour only after ownership is final. The report also asks for a deterministic test with an already-closed socket and an injected delay between bootstrap and client construction, run under ASan and TSan. Its patch sketch for ConnectStream is a comment describing this, with no code.

Assessment

The description matches the code at the pinned commit and the public report. The bug was found on 2026-07-10 in mp#298 CI on NetBSD, where it aborted deterministically with assertion "m_loop" failed, and it could be reproduced elsewhere with an added sleep. The standalone model adds nothing beyond that crash.

The fix is the one the report proposes. On libmultiprocess master, ConnectStream only creates the connection and bootstraps, and ProxyClientBase's constructor registers the delete-on-disconnect handler as its last step ("If this client owns the connection, delete the connection on disconnect."). mp#298 (commit bb473690c97c) merged on 2026-08-11. The report was right that it was unmerged at the commit it examined.

No hostile peer is needed: any server that dies while a client connects can trigger it. In Bitcoin Core the affected processes are short-lived clients such as bitcoin-mine or bitcoin-cli -ipcconnect, and the realistic outcome is a crash of that client.

Bug page: ConnectStream's delete-on-disconnect handler can free the Connection before the ProxyClient that will own it is constructed. The same PR fixed the construct-failure leak described in AR-16.

Status

Fixed in libmultiprocess master by mp#298. Bitcoin Core master still carries the libmultiprocess subtree imported on 2026-07-14, so the fix arrives with the next subtree update. No further action.

Reported privately in an independent security review on 2026-08-07.