Audit report AR-74
Reported privately in an independent security review, 2026-09-28.
- Report date
- 2026-09-28
- Kind
- Hardening
- Status on master
- Present
- Proposed issue state
- Open
Summary
ThreadMap.makePool(count) creates as many operating-system threads as the client asks for, up to about four billion, one at a time on the event loop thread. The report is accurate. It is the same finding as AR-04, narrowed to makePool, and it is hardening rather than a bug: only a client that can already reach the IPC socket, and so already has unauthenticated RPC access, can send the request. The proposed cap of 256 threads per request is a reasonable first step but does not limit makeThread or the total across connections.
The report
makePool is meant to pre-allocate a small pool of server threads for clients that do not name a thread in each call. It accepts any UInt32 count and creates each requested thread synchronously on the event loop, so one short request can use up threads and delay other IPC work, with no per-request limit.
The report rates it low. The trigger needs access to a local IPC socket or the libmultiprocess API, not a peer-to-peer connection. The unbounded loop was established from the source; an oversized request was not run on the unfixed code, so the memory peak and delay were not measured. It notes that partial pool state after a failed thread creation is a separate report, and that a process-wide quota would cover more entry points but needs a broader ownership change.
Proposed change
Reject counts over 256 before creating any thread:
+ static constexpr uint32_t MAX_POOL_THREADS{256};
...
const uint32_t count = context.getParams().getCount();
+ if (count > Connection::MAX_POOL_THREADS) {
+ throw std::runtime_error("makePool count exceeds maximum of " + std::to_string(Connection::MAX_POOL_THREADS));
+ }
A comment in proxy.capnp documents the limit. The test sends makePool(257), expects the error, and then creates and uses an ordinary two-thread pool. The report says the cap is per request, not a process-wide budget: repeated makeThread() calls and multiple connections are still possible, and the threshold could reject an application that wants a larger pool.
Assessment
Accurate. On libmultiprocess master, ProxyServer<ThreadMap>::makePool rejects only a second call on the same connection, then loops up to the client's count on the event loop thread, starting a thread and waiting for it to register on each iteration. The AR-04 reproduction already measured this: one makePool(4000) request created 4000 threads in 176 ms with no error.
This is the same gap as AR-04, which covers makeThread too, and it gets the same assessment. A client that can reach Bitcoin Core's -ipcbind socket already has unauthenticated RPC access, including stop, so the request adds no capability today. Limits matter if less trusted clients are supported later.
The proposed check is correct and small. It runs before any thread is created and its test confirms a normal pool still works. On its own it bounds one request, not the number of threads a client can create, so it fits as the first step of the per-connection limit described in AR-04 rather than a replacement for it. The value 256 is a policy choice; the clients that asked for makePool (mp#283) use small pools.
Status
Present on master. Duplicate of AR-04; the cap belongs in the libmultiprocess issue planned there. No public issue or PR yet.
Reported privately in an independent security review on 2026-09-28.