ListenConnections stops listening for good after one temporary accept() failure such as EMFILE #384

issue ryanofsky opened this issue on October 28, 2026
  1. ryanofsky commented at 1:21 PM on October 28, 2026: member

    If accept() fails once in ListenConnections, even with a temporary error like running out of file descriptors (EMFILE), the listener stops for good: nothing accepts new clients again until the process restarts. If other IPC connections are open, the listening socket stays open and new clients connect but are never served; once those connections close, or right away if there were none, the socket is closed and clients get "connection refused". Temporary errors should be retried after a short delay.

    Where

    _Listen() adds receiver->accept().then(...) to the event loop's task set and only schedules the next accept from inside the success continuation. A failed accept goes to the task set's error handler, which logs "Uncaught exception in daemonized task" and drops the task. Nothing schedules another accept, and the dropped continuation held a std::shared_ptr<Listener>. The only other references are in the disconnect callbacks of open connections, so the receiver and its socket are destroyed when the last of those closes, or right away if there are none.

    kj reports EMFILE, ENFILE, ENOBUFS and ENOMEM from accept() as kj::Exception::Type::OVERLOADED, so these errors are recognizable as temporary.

    Origin

    The missing retry has been there since ListenConnections was added in bitcoin-core/libmultiprocess@d519e1885990 (2019). The accept continuation was then the receiver's only owner, so a failed accept closed the socket at once. #269 (connection limit, merged 2026-07-08) moved the receiver into a shared Listener that open connections also reference, which is why new clients now hang until those connections close. In Bitcoin Core the listener is reachable since -ipcbind was added (bitcoin/bitcoin#30509, v30.0); the hanging variant arrived with the subtree update bitcoin/bitcoin#35684 and is in v32.0.

    Impact on Bitcoin Core

    An -ipcbind listener in bitcoin-node stops serving new IPC clients until restart (they hang while an existing client such as a mining client stays connected, and are refused after that) if the process runs out of descriptors while a client is connecting. The node itself keeps running. bitcoin/bitcoin#35037 now counts IPC listeners and connection slots in the node's descriptor budget, so this needs a descriptor leak or other open files, but the listener should survive it. Other users of the library have no such budget.

    Possible fix

    Catch only OVERLOADED errors from the accept, log a warning once per run of failures (including the error text, so EMFILE can be told apart from ENOMEM), wait briefly on the event loop's timer, and call _Listen() again. Other errors keep ending the listener as they do now, so a permanent failure cannot become a busy loop. A capped backoff (start small, double up to about a second) would be a little friendlier than a fixed delay.

    This is not the blanket retry rejected in #310 (review). That concern was an immediate retry on every error, which would spin on permanent failures and on resource exhaustion; a delayed retry limited to OVERLOADED errors does neither.

    Test

    No descriptor limits need to change. Wrap the real listening socket in a kj::ConnectionReceiver whose first accept() returns an OVERLOADED exception and forwards later calls, pass it to _Listen(), connect a client and check it is served:

    class FailFirstAccept final : public kj::ConnectionReceiver
    {
    public:
        FailFirstAccept(kj::Own<kj::ConnectionReceiver> inner, kj::Exception::Type type)
            : m_inner(kj::mv(inner)), m_type(type) {}
    
        kj::Promise<kj::Own<kj::AsyncIoStream>> accept() override
        {
            if (m_fail) {
                m_fail = false;
                return kj::Exception(m_type, __FILE__, __LINE__, kj::str("simulated accept failure"));
            }
            return m_inner->accept();
        }
    
        uint getPort() override { return m_inner->getPort(); }
    
        kj::Own<kj::ConnectionReceiver> m_inner;
        kj::Exception::Type m_type;
        bool m_fail{true};
    };
    

    With ListenSetup in listen_tests.cpp building the listener from that wrapper, the test fails at once without a fix (no other connections are open, so the socket closes and the client's connect() is refused) and passes with the retry, in about the length of the delay. Checked 2026-10-15 against Bitcoin Core master (bitcoin/bitcoin@badc9574810, which carries libmultiprocess v13 in its subtree). Full test and fix: https://ipcbugs.ofsky.org/bugs/mp-2026-accept-failure-stops-listener/#reproduction

    Reported privately in an independent security review, 2026-10-15 (audit report AR-79).

    Links

  2. ryanofsky added the label bug on Oct 28, 2026
  3. ryanofsky added the label hardening on Oct 28, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/libmultiprocess. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-11-02 00:43 UTC

Static mirror of GitHub issue/PR metadata (via 0xB10C github-metadata-backup), served from ryan.int.chaincode.com.