Skip to content

proxy: add local connection limit to ListenConnections - #269

Merged
ryanofsky merged 3 commits into
bitcoin-core:masterfrom
enirox001:04-26-ipc-local-connection-limit
Jul 8, 2026
Merged

proxy: add local connection limit to ListenConnections#269
ryanofsky merged 3 commits into
bitcoin-core:masterfrom
enirox001:04-26-ipc-local-connection-limit

Conversation

@enirox001

@enirox001 enirox001 commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

This adds an optional local connection limit toListenConnections().

Previously, ListenConnections() would accept incoming connections indefinitely. This branch adds an optional max_connections parameter so a listener can stop accepting new connections once a per-listener cap is reached, and resume accepting when an existing connection disconnects.

The limit is local to the listener instead of global to the EventLoop. This keeps the state and behavior scoped to the listening socket, and is closer to the direction discussed downstream for per--ipcbind limits.

This also adds a test covering the behavior with max_connections=1, verifying that:

  • the first client is accepted normally
  • a second client is not accepted while the first remains connected
  • the second client is accepted after the first disconnects

Note This PR includes a major version bump to v12 due to the API addition. If #274 lands earlier and bumps the version to v12 first, we will need to bump the version here again.

@DrahtBot

DrahtBot commented Apr 8, 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.

Type Reviewers
ACK ryanofsky
Stale ACK xyzconstant, Eunovo

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

Conflicts

No conflicts as of last run.

@enirox001
enirox001 marked this pull request as draft April 8, 2026 10:01
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch 3 times, most recently from 3ef8e5c to 84ed607 Compare April 8, 2026 10:53

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach ACK 84ed607. Implementation of local connection limit here looks almost exactly like I would have expected.

I do think it would be helpful to see a draft PR in the bitcoin repo using this API (it should be fine to make libmultiprocess changes there and let the lint CI job fail so you don't need to mess with subtrees) because the approach in bitcoin/bitcoin#34978 of adding a global connection limit option isn't exactly compatible with the implementation here of implementing a per-address connection limit.

I'd personally prefer using per-address limits over introducing a global limit but both approaches seem reasonable

Comment thread include/mp/proxy-io.h Outdated
Comment thread include/mp/proxy-io.h Outdated
kj::Own<kj::ConnectionReceiver> listener;
std::optional<size_t> max_connections;
size_t active_connections{0};
bool accept_pending{false};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "proxy: add local connection limit to ListenConnections" (84ed607)

Curious if this accept_pending variable is actually necessary or if code could just compare active_connections and max_connections when deciding whether to listen. Would prefer to avoid redundancy in the state representation if possible even if makes individual checks more a little more verbose.

If accept_pending really is necessary would be a good to have a short comment about why.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept accept_pending, but added a short comment explaining why it is needed here. active_connections only counts accepted connections, so without a separate flag nested _Listen() calls could post multiple pending accept() calls before active_connections is incremented.

@xyzconstant xyzconstant May 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I tested the code without accept_pending field and hence its check at _Listen removed, the tests passed with no issues. Then, asked Claude to generate a test that exercises it and produced this:

diff --git a/test/mp/test/listen_tests.cpp b/test/mp/test/listen_tests.cpp
index b367938..78298c7 100644
--- a/test/mp/test/listen_tests.cpp
+++ b/test/mp/test/listen_tests.cpp
@@ -24,8 +24,10 @@
 #include <string>
 #include <sys/socket.h>
 #include <sys/un.h>
+#include <kj/exception.h>
 #include <thread>
 #include <unistd.h>
+#include <vector>
 
 namespace mp {
 namespace test {
@@ -112,11 +114,39 @@ public:
     std::thread thread;
 };
 
+//! kj::ExceptionCallback that captures KJ_LOG output into an external sink.
+//! Must be instantiated on the thread whose KJ logs you want to capture; it
+//! installs itself onto that thread's ExceptionCallback stack via its base
+//! constructor and removes itself in the destructor.
+class CaptureLogCallback : public kj::ExceptionCallback
+{
+public:
+    CaptureLogCallback(std::mutex& mu, std::string& sink) : m_mu(mu), m_sink(sink) {}
+
+    void logMessage(kj::LogSeverity severity, const char* file, int line, int contextDepth,
+                    kj::String&& text) override
+    {
+        {
+            std::lock_guard<std::mutex> lock(m_mu);
+            m_sink.append(text.cStr(), text.size());
+            m_sink.push_back('\n');
+        }
+        // Still let the default callback emit to stderr so test debug output
+        // isn't silenced for other observers.
+        kj::ExceptionCallback::logMessage(severity, file, line, contextDepth, kj::mv(text));
+    }
+
+private:
+    std::mutex& m_mu;
+    std::string& m_sink;
+};
+
 class ListenSetup
 {
 public:
     explicit ListenSetup(std::optional<size_t> max_connections = std::nullopt)
         : capped_listener(max_connections.has_value()), thread([this, max_connections] {
+              CaptureLogCallback log_capture(captured_log_mutex, captured_log);
               EventLoop loop("mptest-server", [this](mp::LogMessage log) {
                   if (log.level == mp::Log::Raise) throw std::runtime_error(log.message);
                   if (log.message.find("IPC server: socket connected.") != std::string::npos) {
@@ -144,6 +174,18 @@ public:
 
     ~ListenSetup()
     {
+        forceShutdown();
+        thread.join();
+    }
+
+    //! Synchronously tear down the event loop's task set so any pending accept
+    //! promises are destroyed now (rather than when the destructor runs later).
+    //! This makes it possible to assert on captured KJ log output before the
+    //! ListenSetup goes out of scope. Idempotent.
+    void forceShutdown()
+    {
+        if (shutdown_done) return;
+        shutdown_done = true;
         if (capped_listener) {
             EventLoop* loop;
             {
@@ -152,7 +194,6 @@ public:
             }
             if (loop) loop->sync([&] { loop->m_task_set.reset(); });
         }
-        thread.join();
     }
 
     size_t ConnectedCount()
@@ -184,11 +225,15 @@ public:
     UnixListener listener;
     std::promise<void> ready_promise;
     bool capped_listener{false};
+    bool shutdown_done{false};
     std::mutex counter_mutex;
     std::condition_variable counter_cv;
     EventLoop* event_loop{nullptr};
     size_t connected_count{0};
     size_t disconnected_count{0};
+    //! KJ log output captured from the server thread via CaptureLogCallback.
+    std::mutex captured_log_mutex;
+    std::string captured_log;
     std::thread thread;
 };
 
@@ -245,6 +290,34 @@ KJ_TEST("ListenConnections keeps capped listeners alive before reaching the limi
     KJ_EXPECT(client2->client->add(2, 3) == 5);
 }
 
+// Without `accept_pending`, cascaded close handlers each post a duplicate
+// accept(). KJ silently serializes them so the cap isn't exceeded, but the
+// extra pending promises are destroyed at cleanup and logged as
+// "PromiseFulfiller was destroyed without fulfilling the promise."
+// This test fails when accept_pending is removed and passes when it's intact.
+KJ_TEST("ListenConnections does not leak accept promises during disconnect burst")
+{
+    constexpr size_t kCap = 2;
+    ListenSetup setup(/*max_connections=*/kCap);
+
+    std::vector<std::unique_ptr<ClientSetup>> filling;
+    filling.reserve(kCap);
+    for (size_t i = 0; i < kCap; ++i) {
+        filling.push_back(std::make_unique<ClientSetup>(setup.listener.Connect()));
+    }
+    setup.WaitForConnectedCount(kCap);
+
+    filling.clear();
+    setup.WaitForDisconnectedCount(kCap);
+
+    // Trigger m_task_set.reset() now so any leaked accept promises get destroyed
+    // before we read captured_log.
+    setup.forceShutdown();
+
+    std::lock_guard<std::mutex> lock(setup.captured_log_mutex);
+    KJ_EXPECT(setup.captured_log.find("PromiseFulfiller was destroyed") == std::string::npos);
+}
+
 } // namespace
 } // namespace test
 } // namespace mp

Basically what this does is install a kj::ExceptionCallback in the server thread to capture the log "PromiseFulfiller was destroyed" generated by KJ runtime if cascaded disconnects each call _Listen and post a fresh accept() promise. The test fails with accept_pending removed and pass with it back.

@xyzconstant xyzconstant May 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's not so obvious why this field is needed here, this patch basically guarantee the same outcome and also pass the test generated by Claude above:

diff --git a/include/mp/proxy-io.h b/include/mp/proxy-io.h
index 78924c6..c002811 100644
--- a/include/mp/proxy-io.h
+++ b/include/mp/proxy-io.h
@@ -851,11 +851,6 @@ struct ListenState
     kj::Own<kj::ConnectionReceiver> listener;
     std::optional<size_t> max_connections;
     size_t active_connections{0};
-    //! Tracks whether accept() has already been posted. This is needed because
-    //! active_connections only counts accepted connections, so without a
-    //! separate flag, nested _Listen() calls could queue multiple pending
-    //! accepts before active_connections increases.
-    bool accept_pending{false};
 };
 
 template <typename InitInterface, typename InitImpl>
@@ -866,9 +861,12 @@ void _ServeAccepted(EventLoop& loop, InitImpl& init, const std::shared_ptr<Liste
 {
     ++state->active_connections;
     _Serve<InitInterface>(loop, kj::mv(stream), init, [&loop, &init, state] {
+        const bool was_at_cap = state->max_connections && state->active_connections == *state->max_connections;
         assert(state->active_connections > 0);
         --state->active_connections;
-        _Listen<InitInterface>(loop, init, state);
+        if (was_at_cap) {
+            _Listen<InitInterface>(loop, init, state);
+        }
     });
 }
 
@@ -885,15 +883,12 @@ inline std::unique_ptr<EventLoopRef> _MakeCappedListenerRef(EventLoop& loop, con
 template <typename InitInterface, typename InitImpl>
 void _Listen(EventLoop& loop, InitImpl& init, const std::shared_ptr<ListenState>& state)
 {
-    if (state->accept_pending) return;
     if (_ListenAtCapacity(*state)) return;
 
-    state->accept_pending = true;
     auto* ptr = state->listener.get();
     auto accept_ref{_MakeCappedListenerRef(loop, *state)};
     loop.m_task_set->add(ptr->accept().then(
         [&loop, &init, state, accept_ref = std::move(accept_ref)](kj::Own<kj::AsyncIoStream>&& stream) mutable {
-            state->accept_pending = false;
             _ServeAccepted<InitInterface>(loop, init, state, kj::mv(stream));
             _Listen<InitInterface>(loop, init, state);
         }));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a better invariant than tracking accept_pending.

There should already be one pending accept whenever the capped listener is below capacity, and disconnects only need to post a new accept when they transition the listener from full to below full, because that is the only state where nocaccept was pending.

So checking this before decrementing active_connections avoids the duplicate-accept case without adding another state variable.

Taken and decided to use resume_accept for the local boolean instead

Comment thread test/mp/test/test.cpp Outdated
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch 4 times, most recently from 57e7070 to 8511c68 Compare April 9, 2026 10:20
@enirox001

Copy link
Copy Markdown
Contributor Author

Thanks for the review @ryanofsky .

I addressed the cleanup points in the latest push:

  • _Serve is now explicit at all call sites
  • accept_pending is documented,
  • listener coverage now lives in a dedicated listen_tests.cpp file introduced in a separate commit before the local-limit extension.

I’m also planning to put together a draft Bitcoin Core PR using this API so the per-address approach can be evaluated downstream against the current global-limit direction.

@enirox001

enirox001 commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

I put together the downstream draft using this API here bitcoin/bitcoin#35037

It uses per--ipcbind max-connections=<n> options, threads the parsed per-address limit into ListenConnections(), and adds downstream coverage for the local-limit behavior.

@enirox001

Copy link
Copy Markdown
Contributor Author

This PR is now ready for review

@xyzconstant xyzconstant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 8511c68

Reviewed each commit separately, compiled and ran tests. The changes look good to me, only left a couple of inline nits noting a compilation error + failing tests in the first commit.

}
});
FooImplementation foo;
ListenConnections<messages::FooInterface>(loop, listener.release(), foo, max_connections);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This will fail to compile mptest at commit 8c47a3a since the 4th parameter max_connections is not introduced in ListenConnections's signature until next commit. I'd fix the call for this commit and then adjust the setup accordingly in 8511c68.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks

KJ_EXPECT(client->client->add(1, 2) == 3);
}

KJ_TEST("ListenConnections enforces a local connection limit")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Unlike in ListenConnections's call issue (compilation error), this will still fail for this commit (8c47a3a). Following ryanosfky's reasoning (#269 (comment)) I believe this suite would be better introduced in 8511c68 so the test lands with the feature it exercises.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@DrahtBot
DrahtBot requested a review from ryanofsky May 13, 2026 03:15
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch from 8511c68 to b36e98b Compare May 14, 2026 08:43
@enirox001

Copy link
Copy Markdown
Contributor Author

Thanks for the review @xyzconstant . Fixed both commit-structure issues: the first test commit now only adds baseline ListenConnections() coverage using the existing 3-argument API, and the max_connections setup/test now lands with the feature commit that introduces the new parameter

@enirox001

enirox001 commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

Also tightened the capped listener behavior. It now stops posting accepts once the limit is reached, and keeps capped pending accepts alive so later clients can connect after an idle gap, including before the cap has been reached.

Added coverage for the reconnect cases as well

@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch from b36e98b to 19e1386 Compare May 14, 2026 08:51
Comment thread include/mp/proxy-io.h Outdated
[&loop, &init, state, accept_ref = std::move(accept_ref)](kj::Own<kj::AsyncIoStream>&& stream) mutable {
state->accept_pending = false;
_ServeAccepted<InitInterface>(loop, init, state, kj::mv(stream));
if (_ListenAtCapacity(*state)) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "proxy: add local connection limit to ListenConnections" (19e1386)

Not sure if I'm missing something but this check here seems redundant with the same _ListenAtCapacity check at the start of _Listen (line 889).

I tested commenting this line out and left the upper-level check (and vice-versa) and the tests passed with no issues. I'd suggest dropping any of these duplicates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. Dropped the lower _ListenAtCapacity() check since _Listen() already handles the capacity check before posting another accept.

@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch from 19e1386 to b0207dd Compare May 19, 2026 09:05
Comment thread include/mp/proxy-io.h Outdated
[&loop, &init, state, accept_ref = std::move(accept_ref)](kj::Own<kj::AsyncIoStream>&& stream) mutable {
state->accept_pending = false;
_ServeAccepted<InitInterface>(loop, init, state, kj::mv(stream));
_Listen<InitInterface>(loop, init, state);

@xyzconstant xyzconstant May 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the accept_pending check in place, this _Listen call will never be reached.

NOTE: if you apply this patch here (comment), then it will be needed here because we can't rely on the second _Listen call in _ServeAccepted which is executed conditionally after active_connections reached the cap.

@xyzconstant

Copy link
Copy Markdown
Contributor

Thanks for the update @enirox001!

I've been playing around with the PR code more throughly this time and have a different take on accept_pending now, so left a few comments.

Overall the code is factually correct and works as expected. And I think it could be merged (despite my latest thoughts on accept_pending). So I'll just re-ACK b0207dd

@enirox001

Copy link
Copy Markdown
Contributor Author

This looks good in it's current form and could be merged. Let me know if you'd prefer that or want to make another round of updates

Thanks for giving this a look, i intend to make another round of updates. Should be able to have that done soon

This bumps the major version to 12 because upcoming commits
introduce a non-trivial feature by adding a local max-connections
parameter to the ListenConnections() method

This also records release notes for v11 in doc/version.md. These notes
are unrelated to this PR and describe changes that will be tagges before
this PR
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch 3 times, most recently from 683bee9 to e4dad41 Compare July 8, 2026 11:47
@enirox001

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews @ryanofsky @Eunovo. Addressed them in the latest commits

I'm also planning to open a new bitcoin core PR bumping the subtree and incorporating this change after this PR is merged

This would be helpful to bitcoin/bitcoin#35037 as well as greatly simplify the work needed for the PR. Looking forward to reviewing this

@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch 2 times, most recently from 95aaf9e to f9ef92e Compare July 8, 2026 12:13
Comment thread test/mp/test/listen_tests.cpp Outdated
Add a separate listen_tests.cpp file with reusable UnixListener, ClientSetup
and ListenSetup helpers for exercising ListenConnections() with real Unix
domain sockets.

The new test covers the baseline behavior that ListenConnections() accepts an
incoming connection and serves requests over it. Keeping this coverage separate
from the existing general proxy tests makes the socket listener setup easier to
review and provides a clearer place to extend listener-specific behavior in
follow-up commits.

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK f9ef92e. Thanks for the updates!Feel free to ignore my comment about whitespace. I only posted it since I noticed CI was failing and thought there would have to be another push

@DrahtBot
DrahtBot requested a review from Eunovo July 8, 2026 12:21
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch from f9ef92e to e63835c Compare July 8, 2026 12:21
Add an optional max_connections parameter to ListenConnections() and track
the limit with listener-local active connection state, so accepting pauses
at capacity and resumes after a disconnect.

Update listener tests for cap enforcement, resume behavior, and multiple
active connections.
@enirox001
enirox001 force-pushed the 04-26-ipc-local-connection-limit branch from e63835c to 39a10ce Compare July 8, 2026 12:30
@enirox001

Copy link
Copy Markdown
Contributor Author

Thanks for the updates!Feel free to ignore my comment about whitespace. I only posted it since I noticed CI was failing and thought there would have to be another push

Addressed these in the recent change. Faced some IWYU errors after the comment above. So had to resolve them anyway. Thanks

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 39a10ce

Just include and whitespace changes since last review

@ryanofsky
ryanofsky merged commit 28e0565 into bitcoin-core:master Jul 8, 2026
13 checks passed
@xyzconstant

Copy link
Copy Markdown
Contributor

Post-merge re-ACK 39a10ce

fanquake pushed a commit to bitcoin/bitcoin that referenced this pull request Jul 10, 2026
28e0565 Merge bitcoin-core/libmultiprocess#269: proxy: add local connection limit to ListenConnections
39a10ce proxy: add local connection limit to ListenConnections()
43172f5 test: add dedicated ListenConnections coverage
033f812 doc/version: Bump version 11 > 12

git-subtree-dir: src/ipc/libmultiprocess
git-subtree-split: 28e0565
fanquake added a commit to bitcoin/bitcoin that referenced this pull request Jul 10, 2026
… option

707d0de Squashed 'src/ipc/libmultiprocess/' changes from 16bf05d..28e0565 (Ryan Ofsky)

Pull request description:

  The changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh).

  Change since last subtree update (#35661):

  - Adds an optional `max_connections` parameter to `ListenConnections` ([#269](bitcoin-core/libmultiprocess#269))

  This is needed for #35037 which lets the maximum number of IPC of incoming connections be configured in bitcoin core.

ACKs for top commit:
  sedited:
    ACK a9d1b65

Tree-SHA512: 1c3ec5c4eb98717c7414a32a3faf63e551b402f7318146745d840fb0e80cbd9e5006892476eb0869eb659cc551ac26d7a3fb2e43509883fb276ac0639a2a7c79
Kino1994 pushed a commit to Kino1994/bitcoin-full-history that referenced this pull request Jul 10, 2026
…ax_connections` option

707d0de Squashed 'src/ipc/libmultiprocess/' changes from 16bf05dea02..28e056576a3 (Ryan Ofsky)

Pull request description:

  The changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh).

  Change since last subtree update (#35661):

  - Adds an optional `max_connections` parameter to `ListenConnections` ([#269](bitcoin-core/libmultiprocess#269))

  This is needed for bitcoin/bitcoin#35037 which lets the maximum number of IPC of incoming connections be configured in bitcoin core.

ACKs for top commit:
  sedited:
    ACK 44f602d

Tree-SHA512: 1c3ec5c4eb98717c7414a32a3faf63e551b402f7318146745d840fb0e80cbd9e5006892476eb0869eb659cc551ac26d7a3fb2e43509883fb276ac0639a2a7c79
Sjors added a commit to Sjors/sv2-tp that referenced this pull request Jul 21, 2026
e8de5c7b68 Merge bitcoin-core/libmultiprocess#305: refactor: memcpy to std::ranges::copy to work around ubsan warn
9307e68e5a Merge bitcoin-core/libmultiprocess#306: doc: Bump version 12 > 13
fac7b9b7f6 refactor: memcpy to std::ranges::copy to work around ubsan warn
1bd7025609 Merge bitcoin-core/libmultiprocess#297: test: add map serialization round-trip coverage
438fdd243d doc: Bump version 12 > 13
28e056576a Merge bitcoin-core/libmultiprocess#269: proxy: add local connection limit to ListenConnections
39a10ce895 proxy: add local connection limit to ListenConnections()
43172f52d9 test: add dedicated ListenConnections coverage
033f812195 doc/version: Bump version 11 > 12
463d073cb8 test: rename vBool to vector_bool
16bf05dea0 Merge bitcoin-core/libmultiprocess#302: refactor: rename EventLoop::m_num_clients to m_num_refs
dd537da9e4 Merge bitcoin-core/libmultiprocess#301: test: recursive async IPC calls and cleanups
400291de00 Merge bitcoin-core/libmultiprocess#299: ci: remove libevent from Core CIs
092be515ad Merge bitcoin-core/libmultiprocess#285: Add ReadList helper
5b617880c5 Merge bitcoin-core/libmultiprocess#283: Add `makePool` method on `ThreadMap`
d499830415 refactor: rename EventLoop::m_num_clients to m_num_refs
6450345c98 type: reserve first when reading std::unordered_set
4d0f8db5f9 proxy: add ReadList helper and dedup map/set/vector read handlers
0e49d91186 Add `makePool` method on `ThreadMap`
5519f7f948 test: recursive async IPC calls
a29ceff40b ci: remove libevent from Core CIs
85df233845 test: add mapStringInt to foo.capnp to cover map serialization and deserialization

git-subtree-dir: src/ipc/libmultiprocess
git-subtree-split: e8de5c7b68e0ae21c94ae92aa22e5c3b213f9c12
Sjors added a commit to Sjors/sv2-tp that referenced this pull request Jul 21, 2026
e8de5c7b68 Merge bitcoin-core/libmultiprocess#305: refactor: memcpy to std::ranges::copy to work around ubsan warn
9307e68e5a Merge bitcoin-core/libmultiprocess#306: doc: Bump version 12 > 13
fac7b9b7f6 refactor: memcpy to std::ranges::copy to work around ubsan warn
1bd7025609 Merge bitcoin-core/libmultiprocess#297: test: add map serialization round-trip coverage
438fdd243d doc: Bump version 12 > 13
28e056576a Merge bitcoin-core/libmultiprocess#269: proxy: add local connection limit to ListenConnections
39a10ce895 proxy: add local connection limit to ListenConnections()
43172f52d9 test: add dedicated ListenConnections coverage
033f812195 doc/version: Bump version 11 > 12
463d073cb8 test: rename vBool to vector_bool
16bf05dea0 Merge bitcoin-core/libmultiprocess#302: refactor: rename EventLoop::m_num_clients to m_num_refs
dd537da9e4 Merge bitcoin-core/libmultiprocess#301: test: recursive async IPC calls and cleanups
400291de00 Merge bitcoin-core/libmultiprocess#299: ci: remove libevent from Core CIs
092be515ad Merge bitcoin-core/libmultiprocess#285: Add ReadList helper
5b617880c5 Merge bitcoin-core/libmultiprocess#283: Add `makePool` method on `ThreadMap`
d499830415 refactor: rename EventLoop::m_num_clients to m_num_refs
6450345c98 type: reserve first when reading std::unordered_set
4d0f8db5f9 proxy: add ReadList helper and dedup map/set/vector read handlers
0e49d91186 Add `makePool` method on `ThreadMap`
5519f7f948 test: recursive async IPC calls
a29ceff40b ci: remove libevent from Core CIs
85df233845 test: add mapStringInt to foo.capnp to cover map serialization and deserialization

git-subtree-dir: src/ipc/libmultiprocess
git-subtree-split: e8de5c7b68e0ae21c94ae92aa22e5c3b213f9c12
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 7, 2026
Fix bitcoin#35845, an assertion failure in MinerImpl::chainman()
during shutdown of an IPC-mining node.

Shutdown() calls disconnectIncoming() before node.chainman.reset().
Disconnecting cancels the KJ promise of an in-flight IPC server call, but a
C++ server method body already dispatched to a libmultiprocess worker thread
is not interrupted and runs to completion. A still-running body (an in-flight
Mining.checkBlock) could then dereference m_node.chainman after
chainman.reset() nulled it, aborting on Assert(m_node.chainman).

Make disconnectIncoming() disconnect the non-parent incoming connections,
wait off the event loop thread for their in-flight server call bodies to
finish (Connection::waitDrained), and only then destroy them and return, so
Shutdown() frees node state only once no server code is running. Log when
the wait actually blocks so a shutdown hang here is diagnosable.

No wait is needed for calls parked in waitTipChanged()/waitNext():
Interrupt() runs before Shutdown() and notifies m_tip_block_cv after setting
the shutdown signal, so those return before disconnectIncoming() runs.

Intentional limitations, to keep the fix narrow: m_impl destructors
scheduled on the async cleanup thread are not waited for, the kept-open
parent connection is not drained, and new incoming connections can still be
accepted during shutdown (preventing that needs a listener API, proposed in
bitcoin-core/libmultiprocess#269).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ryanofsky added a commit that referenced this pull request Aug 11, 2026
…eam`)

fae9a63 example: Remove unused kj/async.h include (xyzconstant)
bb47369 Fix error handling when creating clients (xyzconstant)
44d1914 Add test coverage for ConnectStream (xyzconstant)
231361a Correct stale UnixListener doc comment (xyzconstant)
060c1a5 Extract `UnixListener` class to a dedicated file (xyzconstant)

Pull request description:

  Avoid use-after-free if the socket is disconnected before `ConnectStream` connects (#308), and avoid leaks and hangs if client `construct()` calls throw (#309). Also add tests to cover these and other client connection errors, as [suggested](#183 (comment)) by @ryanofsky.

  The following cases are tested:

  1. Connecting to a socket serving a valid init interface
  2. Passing a disconnected socket (`ConnectStream` throws during the `construct()` call)
  3. Passing a disconnected socket to an interface without `construct()` (the failure is deferred to the first IPC request)
  4. Passing a disconnected socket and making no calls (the disconnect is still handled and the connection cleaned up)
  5. Passing a live socket that disconnects after some data is received
  6. Passing a socket from a listening socket (`accept()`) that disconnects after some data arrives

  Additionally, a new `FooInit` test interface is added, and the `UnixListener` class introduced in #269 is extracted to a shared file so the new `connect_tests.cpp` file can use it.

  Note: Clients that own their connection now delete it on unexpected disconnects, so calls after a server disconnect fail with "called after disconnect" instead of "interrupted by disconnect" (one test.cpp assertion updated accordingly).

ACKs for top commit:
  ryanofsky:
    Code review ACK fae9a63. I left a lot of comments here about the tests, but the fix itself looks very good and this could be merged as-is.

Tree-SHA512: 450c76c4a98163f48a75deac6048d4876fd51d6cc1a6c3983b1f0c5edd29fb25df1ed6d3492e9308ba54cdd55d0bbbd532785d397b7027f4aba1c78b1dadc631
ryanofsky added a commit to ryanofsky/bitcoin that referenced this pull request Aug 12, 2026
Fix bitcoin#35845, an assertion failure in MinerImpl::chainman()
during shutdown of an IPC-mining node.

Shutdown() calls disconnectIncoming() before node.chainman.reset().
Disconnecting cancels the KJ promise of an in-flight IPC server call, but a
C++ server method body already dispatched to a libmultiprocess worker thread
is not interrupted and runs to completion. A still-running body (an in-flight
Mining.checkBlock) could then dereference m_node.chainman after
chainman.reset() nulled it, aborting on Assert(m_node.chainman).

Make disconnectIncoming() disconnect the non-parent incoming connections,
wait off the event loop thread for their in-flight server call bodies to
finish (Connection::waitDrained), and only then destroy them and return, so
Shutdown() frees node state only once no server code is running. Log when
the wait actually blocks so a shutdown hang here is diagnosable.

No wait is needed for calls parked in waitTipChanged()/waitNext():
Interrupt() runs before Shutdown() and notifies m_tip_block_cv after setting
the shutdown signal, so those return before disconnectIncoming() runs.

Intentional limitations, to keep the fix narrow: m_impl destructors
scheduled on the async cleanup thread are not waited for, the kept-open
parent connection is not drained, and new incoming connections can still be
accepted during shutdown (preventing that needs a listener API, proposed in
bitcoin-core/libmultiprocess#269).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kino1994 pushed a commit to Kino1994/bitcoin-full-history that referenced this pull request Aug 19, 2026
…ax_connections` option

707d0de Squashed 'src/ipc/libmultiprocess/' changes from 16bf05dea02..28e056576a3 (Ryan Ofsky)

Pull request description:

  The changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh).

  Change since last subtree update (#35661):

  - Adds an optional `max_connections` parameter to `ListenConnections` ([#269](bitcoin-core/libmultiprocess#269))

  This is needed for bitcoin/bitcoin#35037 which lets the maximum number of IPC of incoming connections be configured in bitcoin core.

ACKs for top commit:
  sedited:
    ACK a9d1b652f324126ef7e80d9ab0b9e4f60019dade

Tree-SHA512: 1c3ec5c4eb98717c7414a32a3faf63e551b402f7318146745d840fb0e80cbd9e5006892476eb0869eb659cc551ac26d7a3fb2e43509883fb276ac0639a2a7c79
vmta added a commit to umkoin/umkoin that referenced this pull request Sep 11, 2026
2dba13047 Merge bitcoin-core/libmultiprocess#363: ci: use LLVM 23 in Bitcoin Core CI
161197a5c Merge bitcoin-core/libmultiprocess#352: ci: add cmake debug output
fb4ac7eb8 ci: use LLVM 23 in Bitcoin Core CI
7bac69de1 Merge bitcoin-core/libmultiprocess#360: pull latest .clang-tidy from downstream
79ddc44eb Merge bitcoin-core/libmultiprocess#359: ci: bump cmake version to 4.3.4 in newdeps job
bf229bf82 Merge bitcoin-core/libmultiprocess#351: ci: do not ignore NIXPKGS_CHANNEL in local ci runs
073ac4f19 Merge bitcoin-core/libmultiprocess#347: refactor: Replace EventLoop::post() with sync() taking kj::FunctionParam
fa1db7a9e pull latest .clang-tidy from downstream
5c49666a1 refactor: rename EventLoop::m_post_fn to m_sync_fn
2330fbe81 refactor: replace EventLoop::post() with sync() taking kj::FunctionParam
4d454a81d Merge bitcoin-core/libmultiprocess#358: refactor: Enable readability-container-contains
dba99582b Merge bitcoin-core/libmultiprocess#357: doc: Update Cap'n Proto version to match minimum
00923922a Merge bitcoin-core/libmultiprocess#356: ci: Remove hard-coded -j4 from sanitize config
766867fb3 ci: clarify CAPNP_CHECKOUT=master is the v1.x release branch
cc3675280 ci: bump cmake version to 4.3.4 in newdeps job
fa101113b refactor: Enable readability-container-contains
81f824b02 doc: Update Cap'n Proto version to match minimum
fa30e2093 ci: Remove hard-coded -j4 from sanitize config
a7ff9d5da ci: add cmake debug output
f2e8df82e Merge bitcoin-core/libmultiprocess#350: cmake: add type-unordered-set.h and version.h to public headers
0e146c046 Merge bitcoin-core/libmultiprocess#349: type-context: fix async disconnect race condition found by antithesis
49f95e263 Merge bitcoin-core/libmultiprocess#212: ci: add newdeps job testing newer versions of cmake and capnproto
7c73cceda ci: rename CI-internal variables to use CI_ prefix
fe1b8339f ci: do not ignore NIXPKGS_CHANNEL in local ci runs
914dc839f proxy: fix data race between server request threads and disconnect handling
275c8eefd Merge bitcoin-core/libmultiprocess#345: Remove trailing whitespace and Add -Wtrailing-whitespace to default ci config
cd7162fb8 Merge bitcoin-core/libmultiprocess#304: proxy: fix BuildList to use non-const iteration for interface types
9b136782a ci: Add -Wtrailing-whitespace to default config
2f4be9ec6 refactor: Remove trailing whitespace
2448d282c cmake: add type-unordered-set.h and version.h to public headers
b3fc922ee ci: add newdeps job testing newest versions of cmake and capnproto
390b5f901 Merge bitcoin-core/libmultiprocess#344: test: listen_tests and connect_tests follow-ups
d6f8588d1 proxy: fix BuildList to use non-const iteration for interface types
e18ca520f Merge bitcoin-core/libmultiprocess#343: test: fix race in connect_tests disconnect-deferred-failure test
c39c7850c doc: note construct() call in valid init interface test
b9c36c617 test: close sockets unconditionally and check errors with KJ_SYSCALL
7eb741e63 test: drop unnecessary KJ_EXPECT(true)
113f1d4d2 test: join server thread unconditionally in connect tests
44bc4630b test: drop mp:: prefixes in connect tests
038d33eb3 test: share DefaultLogHandler between test files
b54a16330 test: drop TestSetup socket members in connect tests
70467c5a7 test: add m_ prefix to TestSetup members in connect tests
cc260f252 test: replace capnp fix link with upstream PR
137a6e4e0 test: fix race in connect_tests disconnect-deferred-failure test
8dab0d4bd Merge bitcoin-core/libmultiprocess#341: ci: add -Wextra-semi to llvm config
b3b134eed ci: add -Wextra-semi to llvm config
bdd0cd694 Merge bitcoin-core/libmultiprocess#339: refactor: add `[[noreturn]]` attributes
a779a0976 ci: add -Wmissing-noreturn
636aaff57 refactor: add missing [[noreturn]] attributes
cc11c2b1b Merge bitcoin-core/libmultiprocess#338: test: check ReadList return value
2d6e863c7 Merge bitcoin-core/libmultiprocess#334: ci: Set CMAKE_BUILD_PARALLEL_LEVEL to enable parallelism by default
d4d10ff98 Merge bitcoin-core/libmultiprocess#332: ci: add -Wextra-semi to default config
b540e70f2 Merge bitcoin-core/libmultiprocess#324: proxy: Name threads spawned by the event loop
e5e367e78 Merge bitcoin-core/libmultiprocess#312: util: report back child errors to parent and throw
2220df68c Merge bitcoin-core/libmultiprocess#298: Fix error handling when creating clients (`mp::ConnectStream`)
51defb79e Merge bitcoin-core/libmultiprocess#340: ci: Update `capnproto` prerequisites on NetBSD
7e94790b0 ci: Update `capnproto` prerequisites on NetBSD
9f25ffca5 test: Cover OS thread names for worker, pool, and async threads
648a18589 proxy: Name threads spawned by the event loop
49834b260 ci: add -Wextra-semi to default config
fae9a637e example: Remove unused kj/async.h include
bb473690c Fix error handling when creating clients
44d191420 Add test coverage for ConnectStream
231361ae5 Correct stale UnixListener doc comment
060c1a50d Extract `UnixListener` class to a dedicated file
62f25af06 test: check ReadList return value
ce51d7372 ci: Set CMAKE_BUILD_PARALLEL_LEVEL to enable parallism in build jobs by default
67302cd13 Merge bitcoin-core/libmultiprocess#331: Remove code for Cap'n Proto versions before 0.9
f13c64ab5 Merge bitcoin-core/libmultiprocess#330: ci: Compile with minimum supported g++ in olddeps
8e026f662 Merge bitcoin-core/libmultiprocess#327: build: avoid unnecessary capnp-rpc dependency for mpgen
e5206e9eb Merge bitcoin-core/libmultiprocess#325: cmake: Remove `QUIET` option from `find_package(CapnProto ...)`
879efea2b Merge bitcoin-core/libmultiprocess#321: ci: Roll NetBSD releases to 11.0, drop 9.4
abf127a31 Merge bitcoin-core/libmultiprocess#317: ipc: Fix mpgen capnp tool path for vcpkg/Windows builds
c437d7f10 Merge bitcoin-core/libmultiprocess#310: test: cover immediate client disconnects for `ListenConnections`
31bff8a67 Merge bitcoin-core/libmultiprocess#307: refactor: memcpy -> std::ranges::copy
f355108b0 Merge bitcoin-core/libmultiprocess#303: type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point
2d678177c Merge bitcoin-core/libmultiprocess#296: ci: Bump channel to nixos-26.05
3f05b1162 util: kill and reap child on SpawnProcess error
4a56c1837 util: report back child error to parent and throw
a9e70dbe7 ci: Add NetBSD release 11.0
2d33b14fb ci: Switch to default compiler on NetBSD 9.4
36f740027 ci: Drop NetBSD release 9.4
bd508311b refactor: Drop stray semicolons after function definitions
788f17a85 Remove code for Cap'n Proto versions before 0.9
7402affd0 ci: Pin oldeps config to older nixpkgs channel to compile older cmake with older gcc
edf634356 ci: Compile with minimum supported g++-11 in olddeps
fa47449af cmake: avoid unnecessary capnp-rpc dependency for mpgen
a494b764d cmake: Remove `QUIET` option from `find_package(CapnProto ...)`
26452e02d refactor: memcpy -> std::ranges::copy
e1dcc6eb1 Merge bitcoin-core/libmultiprocess#316: cmake: Fix stale codegen when mpgen binary changes
7a72df02e type-chrono: Add CustomBuildField/CustomReadField overloads for std::chrono::time_point
45b685c3f type-number, type-chrono: Fix static assert signed/unsigned comparisons
45f625597 type-number: exclude bool from the integral overload
8d6d46494 Merge bitcoin-core/libmultiprocess#315: Fix startup race in example
a6fc80d25 Merge bitcoin-core/libmultiprocess#311: bugfix: clear FD_CLOEXEC in child instead of parent before fork
496fb84e6 test: cover immediate client disconnects for `ListenConnections`
36c6c6352 doc: Document reference-counted EventLoop lifetime
3a997e113 Fix startup race in mpexample
f5c15ce33 Merge bitcoin-core/libmultiprocess#323: refactor: access ThreadContext through CurrentThread(), ci: switch Bitcoin Core to master
66298c737 ci: Switch back to Bitcoin Core's master branch
86b481050 refactor: access ThreadContext through CurrentThread()
eea9c64f6 cmake: Fix stale codegen when mpgen binary changes
a26a08496 cmake: Fix mpgen capnp tool path for vcpkg/Windows builds
140d9ba6f test: allow custom log handler in `ListenSetup`
1e0c7ff9a util: Clear FD_CLOEXEC in child instead of parent before fork
8550ee6a3 util, refactor: Add ChildFail helper for post-fork child errors
17eab90b5 test: Fix typo in listen_tests.cpp
ce865a9ba refactor: Directly use value in CustomBuildField
3f221b5bf Merge bitcoin-core/libmultiprocess#274: Add nonunix platform support
e8de5c7b6 Merge bitcoin-core/libmultiprocess#305: refactor: memcpy to std::ranges::copy to work around ubsan warn
1b0f60560 doc: Remove trailing whitespace
d8f8ca311 ipc: Wrap mpgen main() in try-catch to print errors
fbe5a14ad ci: Check out bitcoin/bitcoin PR #35084 instead of master
39d3690d8 types: Replace SFINAE with requires clauses to avoid MSVC C2039 error
ba6852020 proxy, refactor: Fix C4305 truncation warning in Accessor on MSVC
1d81d4781 util, refactor: Fix PtrOrValue constructor for move-only types on MSVC
b883fe1e5 proxy: Fix shutdownWrite() exception handling on macOS with dynamic libraries
0012411cc proxy: Call shutdownWrite() in Connection destructor
38312ad19 proxy, refactor: Change ConnectStream and ServeStream to accept stream objects
e96d5d742 proxy, refactor: Replace EventLoop wakeup fd integers with KJ stream objects
db4f9a3d7 cmake: Bump minimum required Cap'n Proto version to 0.9
652934fb7 util, refactor: Add SocketPair() and use it in SpawnProcess
1c6ef7a26 util, refactor: Do not fork() and exec() separately
1389cf313 util, refactor: Add SpawnConnectInfo type alias and use it
c7ca1f00b util, refactor: Add SocketId type alias and use it
be46a3520 util, refactor: Add ProcessId type alias and use it
91a78db78 doc: Bump version 13 > 14
9307e68e5 Merge bitcoin-core/libmultiprocess#306: doc: Bump version 12 > 13
fac7b9b7f refactor: memcpy to std::ranges::copy to work around ubsan warn
1bd702560 Merge bitcoin-core/libmultiprocess#297: test: add map serialization round-trip coverage
438fdd243 doc: Bump version 12 > 13
28e056576 Merge bitcoin-core/libmultiprocess#269: proxy: add local connection limit to ListenConnections
39a10ce89 proxy: add local connection limit to ListenConnections()
43172f52d test: add dedicated ListenConnections coverage
033f81219 doc/version: Bump version 11 > 12
463d073cb test: rename vBool to vector_bool
16bf05dea Merge bitcoin-core/libmultiprocess#302: refactor: rename EventLoop::m_num_clients to m_num_refs
dd537da9e Merge bitcoin-core/libmultiprocess#301: test: recursive async IPC calls and cleanups
400291de0 Merge bitcoin-core/libmultiprocess#299: ci: remove libevent from Core CIs
092be515a Merge bitcoin-core/libmultiprocess#285: Add ReadList helper
5b617880c Merge bitcoin-core/libmultiprocess#283: Add `makePool` method on `ThreadMap`
d49983041 refactor: rename EventLoop::m_num_clients to m_num_refs
6450345c9 type: reserve first when reading std::unordered_set
4d0f8db5f proxy: add ReadList helper and dedup map/set/vector read handlers
0e49d9118 Add `makePool` method on `ThreadMap`
5519f7f94 test: recursive async IPC calls
a29ceff40 ci: remove libevent from Core CIs
85df23384 test: add mapStringInt to foo.capnp to cover map serialization and deserialization
fa2c56ec2 ci: Bump channel to nixos-26.05
8412fcdc6 Merge bitcoin-core/libmultiprocess#295: Mark Waiter m_cv as guarded by m_mutex
1593ee2d1 Merge bitcoin-core/libmultiprocess#294: test: Add passDouble smoke test
9885d7dd3 Merge bitcoin-core/libmultiprocess#286: proxy-client: fix TSan data race in clientDestroy
fa35501c4 Mark Waiter m_cv as guarded by m_mutex
faaedb11f test: Add passDouble smoke test
733c64318 Merge bitcoin-core/libmultiprocess#292: type-number: fix clang-tidy modernize-use-nullptr
9cc3479ab Merge bitcoin-core/libmultiprocess#291: cmake: Add `mp_headers` custom target
201abd9e3 Merge bitcoin-core/libmultiprocess#289: cmake: make target_capnp_sources use CURRENT dirs
99820c8ae Merge bitcoin-core/libmultiprocess#279: doc: Add comments to FIELD_* constants in proxy.h
73b985540 Merge bitcoin-core/libmultiprocess#278: doc: Fix and expand design.md
e7e91b2e2 Merge bitcoin-core/libmultiprocess#277: Add std::unordered_set support and a helper BuildList to dedup list build handlers
91a951f59 tidy fix: modernize-use-nullptr
16362f42d cmake: Add `mp_headers` custom target
615a94fe3 cmake: document ONLY_CAPNP option in target_capnp_sources
90982f75c mpgen: iwyu changes required by previous commit
25bb3e67f proxy-client: fix TSan data race in clientDestroy
620f297f3 cmake: make target_capnp_sources use CURRENT dirs
9de4b885a test: use camelCase + $Proxy.name for FooStruct fields
011b91793 type: add std::unordered_set support
20d19b964 proxy: add BuildList helper and dedup map/set/vector build handlers
e863c6cdf doc: Add comments to FIELD_* constants in proxy.h
18db0ab95 doc: Fix and expand design.md

git-subtree-dir: src/ipc/libmultiprocess
git-subtree-split: 2dba130478ab71a745fe96c5726719fe357e6d17
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.

6 participants