proxy-io: Fix theoretical disconnect bugs #361

pull ryanofsky wants to merge 5 commits into bitcoin-core:master from ryanofsky:pr/ondis changing 8 files +310 −71
  1. ryanofsky commented at 8:24 PM on September 4, 2026: collaborator

    Two bugs in Connection::onDisconnect were pointed out in #335 review by @enirox001:

    A third "thread map teardown race" bug was also encountered in a new unit test introduced by #335. This PR fixes each bug in a separate commit and also includes documentation commits to help make the changes more understandable.

    The fixed bugs are "theoretical" just in the sense that they haven't been seen in practice and were found in code review. The first ListenConnections bug can't currently happen in bitcoin core because it doesn't disconnect IPC clients except when it is shutting down, and it would not make sense to accept new connections. The race condition bugs have just existed for many years and not been seen previously.

  2. doc: Improve disconnect callback comments
    This is a documentation-only change meant to make upcoming commits easier to
    understand.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    9f0df73280
  3. DrahtBot commented at 8:24 PM on September 4, 2026: none

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Stale ACK enirox001

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #365 (proxy-io: Generalize ConnectStream / ServeStream for use in tests by ryanofsky)
    • #335 (proxy-io.h: Add Connection disconnect and waitDrained methods by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. in include/mp/proxy-io.h:624 in c5f093e48c
     620 | @@ -615,7 +621,7 @@ ProxyClientBase<Interface, Impl>::ProxyClientBase(typename Interface::Client cli
     621 |          m_context.loop->sync([&] {
     622 |              EventLoop& loop = *m_context.loop;
     623 |              Connection* connection = m_context.connection;
     624 | -            connection->onDisconnect([&loop, connection] {
     625 | +            connection->onRemoteDisconnect([&loop, connection] {
    


    enirox001 commented at 2:29 PM on September 9, 2026:

    In commit https://github.com/bitcoin-core/libmultiprocess/pull/361/changes/c5f093e48c0fd263fec82b84d83c6f8bb92abe39: proxy-io: rename Connection disconnect handlers to reflect scope

    Good call on changing the name of the methods to match what they actually run, it a bit weird seeing a familiar name reused for a different method, but should become familiar soon enough

    In proxy.cpp there are some documentation that still references the previous method behaviours, here is a diff that would update them

    index afb02ce..e77c836 100644
    --- a/src/mp/proxy.cpp
    +++ b/src/mp/proxy.cpp
    @@ -111,7 +111,7 @@ Connection::~Connection() noexcept(false)
         // Connection destructor is always called on the event loop thread. If this
         // is a local disconnect, it will trigger I/O, so this needs to run on the
         // event loop thread, and if there was a remote disconnect, this is called
    -    // by an onDisconnect callback directly from the event loop thread.
    +    // by an onRemoteDisconnect callback directly from the event loop thread.
         assert(std::this_thread::get_id() == m_loop->m_thread_id);
    
         // Try to cancel any calls that may be executing.
    @@ -187,14 +187,14 @@ Connection::~Connection() noexcept(false)
         // ProxyServer object destructors first, and then trigger an onDisconnect
         // callback.
         //
    -    // On incoming side of the connection, the onDisconnect callback is written
    +    // On incoming side of the connection, the onRemoteDisconnect callback is written
         // to delete the Connection object from the m_incoming_connections and call
    -    // this destructor which calls Connection::disconnect.
    +    // this destructor.
         //
         // On the outgoing side, the Connection object is owned by top level client
    -    // object client, which onDisconnect handler doesn't have ready access to,
    -    // so onDisconnect handler just calls Connection::disconnect directly
    -    // instead.
    +    // object client, which onRemoteDisconnect handler doesn't have ready access to,
    +    // so onRemoteDisconnect handler just deletes the Connection object directly
    +    // instead
         //
         // Either way disconnect code runs in the event loop thread and called both
         // on clean and unclean shutdowns. In unclean shutdown case when the
    

    ryanofsky commented at 7:53 PM on September 11, 2026:

    re: #361 (review)

    Thanks! I went though and updated all comments. I did wind up dropping the renames in this PR because it caused test_bitcoin to hang in https://github.com/bitcoin-core/libmultiprocess/actions/runs/33916092756, because bitcoin core tests are currently calling onDisconnect. So I moved the rename change to #336 and will be opening a new PR to simplify bitcoin core test code to use ConnectStream & ServeStream instead of low level onDisconnect code

  5. in include/mp/proxy-io.h:910 in bb21177965
     906 | +    // only fires on a remote disconnect and is canceled when a connection is
     907 | +    // closed locally; if on_disconnect lived there, closing a connection
     908 | +    // locally would leave the listener's slot count stuck and stop it from
     909 | +    // accepting again.
     910 | +    it->onDisconnect(std::forward<OnDisconnect>(on_disconnect));
     911 | +    it->onRemoteDisconnect([&loop, it]() mutable {
    


    enirox001 commented at 3:24 PM on September 9, 2026:

    In commit https://github.com/bitcoin-core/libmultiprocess/pull/361/changes/bb21177965e363b32878f8258cc2bc1f9a8661f4: _proxy-io: fix listener stuck at capacity after a local disconnect _

    there is a documetation inconsitency though, it says in proxy.cpp in the Connection::onDisconnect method that "these callbacks only resets the connection pointers" but this commit adds a callback tthat also updates the listener count, so this description is a bit narrow

    would suggest

    index afb02ce..541948a 100644
    --- a/src/mp/proxy.cpp
    +++ b/src/mp/proxy.cpp
    @@ -214,11 +214,10 @@ CleanupIt Connection::onDisconnect(std::function<void()> fn)
         // Add cleanup callbacks to the front of list, so sync cleanup functions run
         // in LIFO order. This is a good approach because sync cleanup functions are
         // added as client objects are created, and it is natural to clean up
    -    // objects in the reverse order they were created. In practice, however,
    -    // order should not be significant because the cleanup callbacks run
    -    // synchronously in a single batch when the connection is broken, and they
    -    // only reset the connection pointers in the client objects without actually
    -    // deleting the client objects.
    +    // objects in the reverse order they were created. The cleanup callbacks run
    +    // synchronously on the event loop thread. They reset connection pointers in
    +    // client objects without deleting the client objects, and update listener
    +    // bookkeeping so freed connection slots can be reused.
         return m_sync_cleanup_fns.emplace(m_sync_cleanup_fns.begin(), std::move(fn));
     }
    

    ryanofsky commented at 7:54 PM on September 11, 2026:

    re: #361 (review)

    Nice suggestion, expanded the comment to mention the new change.

  6. enirox001 commented at 3:42 PM on September 9, 2026: contributor

    ACK bc98767dadb4d67b7cb6c87ca86696a7f888d715

    Thnaks for opening this @ryanofsky, this does address the issues i mentioned, i agree these improvements here are more conservative as they have not been seen in practice, but would be nice to have it if we want to make disconnection more robust.

    Left some documentation nits

  7. doc: Improve Waiter/EventLoop lock order documentation
    Correct the ThreadContext "Synchronization note", which said
    Waiter::m_mutex must not be locked before EventLoop::m_mutex. That is
    the reverse of the documented and actual lock order (Waiter::m_mutex
    first, as ~ProxyServer<Thread> does). The constraint it was reaching for
    is the EventLoop blocking rule now documented on Waiter::m_mutex.
    
    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    019b699bc9
  8. proxy-io: fix listener stuck at capacity after a local disconnect
    Currently, a ListenConnections listener that reaches its max-connection limit
    stops accepting new connections permanently if one of its connections is closed
    locally instead of by a remote disconnect. Closing a connection locally (e.g.
    erasing it from m_incoming_connections) leaves the listener's active-connection
    count stuck at the limit, so it never resumes accepting.
    
    This happens because the count is decremented by a callback which only fires on
    a remote disconnects, not local disconnects. Fix by moving the decrement to
    callback which fires on both local and remote disconnects.
    
    Add a regression test that closes a connection locally and checks the listener
    resumes accepting; it fails before this change (the listener never accepts the
    waiting client) and passes after.
    
    Co-Authored-By: Enoch Azariah <enirox001@gmail.com>
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    35999c779a
  9. proxy-io: fix race deleting a disconnected Connection twice
    Fix a use-after-free, possible since the destroy_connection option was added
    in 2019 (c685fa9): a Connection's disconnect handler could run after the
    Connection had already been destroyed, deleting it a second time and
    crashing. Reported by enirox001 in
    https://github.com/bitcoin-core/libmultiprocess/pull/335#discussion_r3821831654
    
    Give each Connection a shared_ptr "alive" token that disconnect handlers hold
    a weak_ptr to and check before running, so a handler is skipped once its
    Connection is gone. Having this check also enables the simplifications
    described below.
    
    Previously each Connection kept its disconnect handlers in its own
    kj::TaskSet, and when the network disconnected it moved a handler onto the
    shared event loop TaskSet with kj::evalLater. Destroying the Connection
    destroyed that per-connection TaskSet, canceling a still-pending handler --
    but a handler already moved onto the shared TaskSet was no longer canceled
    and could run after the Connection was gone. (The evalLater step existed only
    to avoid a "promise callback destroyed itself" error when a handler deletes
    its own Connection, which the per-connection TaskSet made possible.)
    
    With the token doing the cancellation, neither the per-connection TaskSet nor
    the evalLater step is needed, and both are removed.
    
    Co-Authored-By: Enoch Azariah <enirox001@gmail.com>
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
    088242bb08
  10. proxy: Fix thread map teardown race causing use-after-free on disconnect
    Fix a race between a thread exiting after making IPC calls and its
    connection being destroyed on the event loop thread, which could destroy
    the same ProxyClient<Thread> object twice. ~ThreadContext destroyed the
    thread-local request_threads/callback_threads maps with no locking while
    the SetThread cleanup callback run by ~Connection erased entries from the
    same maps. When both ran at once, each side destroyed the entry's
    ProxyClient<Thread>, and ~Connection then ran the ProxyClientBase
    disconnect callback on the freed map node (heap-use-after-free, then a
    glibc "double free or corruption" abort).
    
    Fix by making map entry removal decide which side destroys an entry:
    ~ThreadContext and the SetThread callback each remove entries under
    Waiter::m_mutex before destroying them, and a side that finds an entry
    already gone leaves it to the other. See the code comments for why the
    entries are destroyed with the mutex released.
    
    Add a regression test, "Thread exiting while its connection is
    destroyed", which uses a new testing_hook_thread_client_destroy hook to
    interleave the two sides deterministically and fails on every run
    without the fix.
    
    The race is long-standing and reachable on master via connections
    created by ConnectStream, whose onDisconnect handler deletes the client
    Connection on the event loop thread when the peer disconnects.
    
    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    040919b0b7
  11. ryanofsky force-pushed on Sep 11, 2026
  12. ryanofsky commented at 7:57 PM on September 11, 2026: collaborator

    <!-- begin push-2 -->

    Updated bc98767dadb4d67b7cb6c87ca86696a7f888d715 -> 0f5c633661c5068cfcb57dcdad21a1af6e6e275c (pr/ondis.1 -> pr/ondis.2, compare)<!-- end --> dropping renames because they cause problems downstream in test_bitcoin, making more documentation updates and adding a third bugfix here "thread map teardown race" that was originally in #335, since it fixes another bug that precedes #335

    <!-- begin push-3 -->

    Updated 0f5c633661c5068cfcb57dcdad21a1af6e6e275c -> 040919b0b7b4855f2679bad80398cc5975d45155 (pr/ondis.2 -> pr/ondis.3, compare)<!-- end --> to fix IWYU error https://github.com/bitcoin-core/libmultiprocess/actions/runs/34640736556/job/103399475344?pr=361

  13. ryanofsky renamed this:
    proxy-io: Fix theoretical Connection::onDisconnect bugs
    proxy-io: Fix theoretical disconnect bugs
    on Sep 11, 2026
  14. ryanofsky force-pushed on Sep 14, 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-09-16 09:30 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me