Audit report AR-19

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

Report date
2026-08-16
Kind
Hardening
Status on master
Fixed in bitcoin#35037
Proposed issue state
Closed
Linked bugs
Bug 78: Listener at its connection limit stops accepting after a local disconnect

Summary

Until 2026-10-08, Bitcoin Core's -ipcbind listeners accepted any number of simultaneous connections: libmultiprocess has an optional per-listener limit, but Bitcoin Core did not pass one, so each accepted client kept a descriptor and server state until it disconnects. The report is accurate and careful to say it shows missing admission control, not a practical denial of service. Any process that can reach the socket already has unauthenticated RPC access, so a cap is hardening. The report itself defers to the public bitcoin#35037, which has since fixed it.

The report

Each -ipcbind listener calls libmultiprocess's ListenConnections() without its optional connection limit. The event loop therefore keeps accepting clients, adds each connection to m_incoming_connections, and removes it only on disconnect or shutdown. The listen() backlog of five limits only clients waiting to be accepted, not connections already accepted. Each accepted client retains a file descriptor, a libmultiprocess Connection, RPC state and list membership, and several listeners multiply the number of admission paths. Connections and disconnections are logged.

The report traces -ipcbind to bitcoin#30509 (Bitcoin Core 29.0) and finds the unlimited call in every release from 29.0 through 31.1 and on master. The actor needs local access to an operator-enabled socket and must keep earlier clients open while connecting more; -ipcbind is off by default, and its help text warns that socket access grants unauthenticated RPC access. No descriptor or memory threshold, crash, RPC failure, shutdown failure or lasting denial of service was reproduced, and stopping the clients or restarting the node releases everything.

The reproduction is a functional test added to test/functional/interface_ipc_cli.py, run against a real node. It restarts the node with a limit of two and a second listener, opens two raw Unix-socket clients on each listener, checks that a third client on the first listener is not accepted, then closes one client and checks that the waiting client is accepted. Removing only the limit argument makes the third connection get accepted, matching master's behaviour.

Proposed change

The patch adds a single global option and passes it to every listener:

+        argsman.AddArg("-ipcmaxconnections=<n>", "Set the maximum number of simultaneous connections accepted on each IPC listening socket (1-1000, default: 16)", ...);
@@ void listen(mp::SocketId listen_fd, interfaces::Init& init) override
+        const int64_t max_connections{gArgs.GetIntArg("-ipcmaxconnections", 16)};
+        if (max_connections < 1 || max_connections > 1000) {
+            throw std::runtime_error("-ipcmaxconnections must be between 1 and 1000");
+        }
@@
-        mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init);
+        mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init, static_cast<size_t>(max_connections));

The test also checks that 0 and 1001 are rejected at startup. The report says this patch should not be merged as a competing fix. bitcoin#35037 already covers the integration more completely: it attaches max-connections= to each -ipcbind address so listeners can have different policies, validates individual and combined limits, reserves descriptors at startup, and documents the default. It notes that neither design bounds work inside one connection (see AR-22 and AR-23), that excess clients wait in the kernel backlog and a full backlog can still delay or reject legitimate clients, and that the interaction with stopping a listener (AR-21) needs testing.

Assessment

Accurate when reported. Until bitcoin#35037, src/ipc/capnp/protocol.cpp called mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init), and libmultiprocess's ListenConnections takes std::optional<size_t> max_connections = std::nullopt since mp#269. This is a resource-limit gap, not a stability bug: reaching it takes a client opening connections on purpose, and such a client can already call stop. bitcoin#34978 proposed reserving descriptors for IPC connections separately; it was closed, and bitcoin#35037 does the reservation.

The limit logic in libmultiprocess has its own open bug, which the survey suggested fixing before a limit was turned on by default: a listener at its max-connection limit never accepts again after the server closes one of its connections locally, fixed in the open mp#361.

AR-09 reports the same gap independently. Related limits: AR-04 (threads), AR-07 (per-method quotas).

Status

Fixed on master by bitcoin#35037 (merged 2026-10-08 as 442884f5f3b): each -ipcbind address takes a max-connections= limit (default 16, at most 256 in total), protocol.cpp passes it to ListenConnections, and startup counts the IPC listeners and connection slots in the node's file descriptor budget. It landed before the mp#361 fix for the listener that stops accepting at its limit, but that bug still cannot be triggered on master: Bitcoin Core closes connections locally only at shutdown.

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