Skip to content

proxy-io: Fix theoretical Connection::onDisconnect bugs - #361

Open
ryanofsky wants to merge 3 commits into
bitcoin-core:masterfrom
ryanofsky:pr/ondis
Open

proxy-io: Fix theoretical Connection::onDisconnect bugs#361
ryanofsky wants to merge 3 commits into
bitcoin-core:masterfrom
ryanofsky:pr/ondis

Conversation

@ryanofsky

Copy link
Copy Markdown
Collaborator

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

This PR fixes each issue in a separate commit and also includes a renaming commit to name disconnect callback methods more clearly.

Both 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 second race condition bug has just existed for many years and not been seen previously.

ryanofsky and others added 3 commits September 3, 2026 15:23
Two Connection callback-registration methods had names that did not match
when they actually run:

- onDisconnect() only ran its handler on a *remote* disconnect (it is
  canceled when the connection is disconnected locally), so rename it to
  onRemoteDisconnect(), and rename its backing TaskSet m_on_disconnect to
  m_on_remote_disconnect.

- addSyncCleanup()/removeSyncCleanup() registered a function that runs on
  *any* disconnect (it is invoked from the connection teardown path), so
  rename them to onDisconnect()/cancelOnDisconnect().

Pure rename, no behavior change. The honest names make the following commits
easier to follow: the next commit moves a listener bookkeeping callback from
onRemoteDisconnect() to onDisconnect() so it runs on local disconnects too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A ListenConnections listener that reached its max-connection limit would stop
accepting new connections permanently if one of its connections was closed
locally instead of by a remote disconnect. Closing a connection locally (e.g.
erasing it from m_incoming_connections) left the listener's active-connection
count stuck at the limit, so it never resumed accepting.

The count is decremented by the on_disconnect callback, which ran from the
_Serve onRemoteDisconnect handler. That handler only fires on a remote
disconnect and is canceled when a connection is closed locally, so the
decrement was skipped on local closes. Register on_disconnect with
onDisconnect() instead, so it runs on the connection teardown path for both
remote and local disconnects, and keep only the list erase on
onRemoteDisconnect().

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>
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
bitcoin-core#335 (comment)

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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DrahtBot

DrahtBot commented Sep 4, 2026

Copy link
Copy Markdown

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

Reviews

See the guideline and AI policy for information on the review process.
A summary of reviews will appear here.

LLM Linter (✨ experimental)

Possible typos and grammar issues:

  • able free Connection objects -> able to free Connection objects [missing “to”; this breaks the sentence]

2026-09-04 20:24:53

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants