Audit report AR-21

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

Report date
2026-08-16
Kind
API
Status on master
Present
Proposed issue state
Open
Linked bugs
Bug 78: Listener at its connection limit stops accepting after a local disconnect

Summary

libmultiprocess's ListenConnections() returns nothing, so an application cannot stop accepting on one listening socket without tearing down the whole event loop. The report is accurate and describes an API gap, not a failure. Bitcoin Core only stops listening at shutdown, so it is not affected today; the gap matters if IPC listen addresses ever change at runtime. The proposed change matches an approach already suggested in libmultiprocess review. Feature request.

The report

An embedding application should be able to stop admitting new clients on one socket, for example before draining that listener's clients, but ListenConnections() returns no handle, and its accept loop has no stopped state. _Listen() posts an asynchronous accept and posts another after each accepted connection, and a listener with a connection limit also resumes accepting whenever an active client disconnects. With no handle and no stop transition, the only way to end admission is to tear down broader event-loop state. The report finds this in the libmultiprocess bundled with Bitcoin Core 30.0 through 31.1.

It describes the impact as an API and lifecycle limitation, not a reproduced Bitcoin Core availability failure: Bitcoin Core listens only on configured -ipcbind addresses and stops them as part of overall shutdown. A process able to reach such a socket already has broad unauthenticated IPC authority. No hang, crash or service after exit was shown. It notes that its change is not wired into Bitcoin Core shutdown: CapnpProtocol::listen() ignores the new return value, and disconnectIncoming() still removes active clients without stopping listeners first.

The reproduction is a new KJ test against the real library, "ListenConnections does not resume accepting after stop". It connects one client, stops the listener, disconnects the client (which would normally resume accepting), connects another socket, and checks that the server never accepts it. With the stopped-state check removed, the listener stays active and accepts the second connection.

Proposed change

ListenConnections returns the Listener, which gains an accepting flag, and a new StopListening clears it on the event-loop thread:

+    bool m_accepting{true};
@@ void _Listen(const std::shared_ptr<Listener>& listener, EventLoop& loop, InitImpl& init)
-    if (listener->atCapacity()) return;
+    if (!listener->m_accepting || listener->atCapacity()) return;
@@
         [&loop, &init, listener](kj::Own<kj::AsyncIoStream>&& stream) {
+            if (!listener->m_accepting) return;
@@
-void ListenConnections(EventLoop& loop, int fd, InitImpl& init, std::optional<size_t> max_connections = std::nullopt)
+std::shared_ptr<Listener> ListenConnections(EventLoop& loop, int fd, InitImpl& init, std::optional<size_t> max_connections = std::nullopt)
@@
+inline void StopListening(EventLoop& loop, const std::shared_ptr<Listener>& listener)
+{
+    loop.sync([&] { listener->m_accepting = false; });
+}

A stopped listener neither posts new accepts nor serves a connection that an already-pending accept returns, and disconnect callbacks no longer resume accepting. It does not cancel the pending accept or close the listening socket. The test fixture keeps the returned handle and releases it on the event-loop thread.

Assessment

Accurate. On libmultiprocess master, ListenConnections returns void and Listener has no stop state. The need was discussed in the review of mp#269, which added the connection limit. ryanofsky, replying to the report, called it "a nice libmultiprocess change fixing a known issue", said it is not an issue for Bitcoin Core now because Bitcoin Core only stops listening during shutdown, and said the implementation is basically what was suggested there and looks reasonable. No stability effect today.

The change should interact correctly with the connection limit: a listener at capacity has no pending accept, and a stopped listener must not resume one when a client disconnects. The same listener code has an open bug, a listener at its max-connection limit never accepts again after the server closes one of its connections locally (fix in mp#361), so the two changes need testing together. Related: AR-09 and AR-19 (connection limits).

Status

Present on master. Next step: open this as a libmultiprocess PR (it modifies the subtree, so it should land upstream first), tested together with the connection-limit logic. No public PR yet.

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