Fix error handling when creating clients (`mp::ConnectStream`) #298

pull xyzconstant wants to merge 5 commits into bitcoin-core:master from xyzconstant:add-coverage-for-connect-stream changing 9 files +369 −72
  1. xyzconstant commented at 3:53 AM on June 24, 2026: contributor

    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 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).

  2. DrahtBot commented at 3:54 AM on June 24, 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
    ACK ryanofsky

    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:

    • #336 (proxy-io: Reference-count Connection objects by ryanofsky)
    • #304 (proxy: fix BuildList to use non-const iteration for interface types by ryanofsky)
    • #231 (Add windows support 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-->

  3. xyzconstant renamed this:
    WIP: Add test coverage for ConnectStream
    WIP: Add test coverage for `ConnectStream`
    on Jun 24, 2026
  4. ryanofsky commented at 8:21 PM on July 5, 2026: collaborator

    Thanks for following up to #183 with these tests. They do seem potentially useful. Here is feedback I'd have:

    • The first 3 tests seems like they are mostly in good shape. You would just want to wrap these in try/catch to and assert expected exceptions are thrown. For the second test it would seem fine to accept both "called after disconnect" and "interrupted by disconnect" exceptions since we already have existing tests testing for each of these errors more specifically.

    • The first 3 tests do overlap a lot with disconnect tests already in test.cpp, and it's a little questionable if having all 3 tests adds much value. The first test could be is nice because it directly tests connecting to a non-capnp server that disconnects ignoring whatever is sent. But the second and third tests just sending the same disconnect at later points in time and needing MSG_PEEK complexity would not seem to add as much value.

    • For the 4th test having the client hang as long as server hangs is probably expected behavior. It would be good to make sure that client can still disconnect or cancel the calls if the the server hangs. Or that client calls are able to time out correctly.

    • It could make sense to rebase this on #269 or rebase if that could help with the "Add the case with an actual mkdtemp/socket/bind/listen setup" follow up comment.

    • Maybe (not sure) it could be interesting to have tests working in opposite direction with server providing dummy interface and clients connecting and disconnecting suddenly. Maybe it could also be useful to have tests sending garbage bytes and making sure clients and servers handle them cleanly. It might even be useful to use fuzzing for this though it might not be a good use of fuzzing resources since it would mostly only be fuzz testing capnproto code and only a little bit of libmultiprocess exception/cleanup handling code.

    Overall the tests here seem reasonable to add. It seems good to have at least 1-2 tests verifying disconnects are processed when a capnproto client connects to a non-capnproto server.

    (Relatedly, there are also other disconnect tests that could be added at different points during capnproto connections, which I started to write in #201 (comment) and https://github.com/ryanofsky/libmultiprocess/commits/pr/distest.2 but was never able to really finish due to complexity of trying to set up and cover all of the relevant cases. Just mentioning this for completeness, though. There's probably not an obviously place to follow up with this at the moment.)

  5. xyzconstant force-pushed on Jul 8, 2026
  6. xyzconstant force-pushed on Jul 8, 2026
  7. xyzconstant force-pushed on Jul 8, 2026
  8. DrahtBot added the label Needs rebase on Jul 8, 2026
  9. xyzconstant force-pushed on Jul 8, 2026
  10. DrahtBot removed the label Needs rebase on Jul 8, 2026
  11. xyzconstant force-pushed on Jul 8, 2026
  12. xyzconstant force-pushed on Jul 8, 2026
  13. xyzconstant force-pushed on Jul 8, 2026
  14. xyzconstant force-pushed on Jul 8, 2026
  15. xyzconstant renamed this:
    WIP: Add test coverage for `ConnectStream`
    Add test coverage for `ConnectStream`
    on Jul 8, 2026
  16. xyzconstant marked this as ready for review on Jul 8, 2026
  17. xyzconstant force-pushed on Jul 8, 2026
  18. xyzconstant force-pushed on Jul 8, 2026
  19. xyzconstant commented at 9:54 PM on July 8, 2026: contributor

    Thanks for the feedback @ryanofsky!

    I've just rebased to master now that #269 has been merged and added a new commit to move UnixListener to its own dedicated file.

    Also, I've made changes to the tests (please check the description), keeping the first 2 tests (Notice that I've dropped the MSG_PEEK setup as you suggested.) now catching errors in a try/catch block, plus a Unix domain socket that disconnects after some data arrives and a successful case connecting to a valid libmultiprocess server.

    It would be good to make sure that client can still disconnect or cancel the calls if the the server hangs. Or that client calls are able to time out correctly.

    This is my current focus. I'm looking more into it, but I think this work might need its own branch so we keep test coverage scoped to this PR.

    Maybe (not sure) it could be interesting to have tests working in opposite direction with server providing dummy interface and clients connecting and disconnecting suddenly. Maybe it could also be useful to have tests sending garbage bytes and making sure clients and servers handle them cleanly. It might even be useful to use fuzzing for this though it might not be a good use of fuzzing resources since it would mostly only be fuzz testing capnproto code and only a little bit of libmultiprocess exception/cleanup handling code.

    Good, these ideas are great and definitely worth exploring. Happy to tackle them after this.

  20. xyzconstant commented at 9:55 PM on July 8, 2026: contributor

    This PR is now ready for review. I've updated the description as well!

  21. xyzconstant force-pushed on Jul 8, 2026
  22. xyzconstant force-pushed on Jul 8, 2026
  23. xyzconstant force-pushed on Jul 8, 2026
  24. xyzconstant force-pushed on Jul 9, 2026
  25. xyzconstant force-pushed on Jul 9, 2026
  26. xyzconstant force-pushed on Jul 9, 2026
  27. xyzconstant force-pushed on Jul 9, 2026
  28. xyzconstant force-pushed on Jul 9, 2026
  29. xyzconstant force-pushed on Jul 10, 2026
  30. xyzconstant commented at 5:44 AM on July 10, 2026: contributor

    The netbsd 9.4 job exposed a use-after-free race condition in ConnectStream on early disconnects, included a new commit in this PR that fixes it.

  31. in test/mp/test/connect_tests.cpp:58 in a648c5120f
      53 | +            loop_promise.set_value(&loop);
      54 | +            loop.loop();
      55 | +        });
      56 | +        loop = loop_promise.get_future().get();
      57 | +
      58 | +        // Initalize and store sockets
    


    maflcko commented at 6:20 AM on July 10, 2026:

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    Initalize -> Initialize [misspelling in the socket setup comment; intended meaning is clear but the word is misspelled]

    2026-07-10 05:29:42


    xyzconstant commented at 2:50 PM on July 10, 2026:

    Thanks!

  32. xyzconstant force-pushed on Jul 10, 2026
  33. xyzconstant commented at 4:09 PM on July 10, 2026: contributor

    The netbsd 9.4 job exposed a use-after-free race condition in ConnectStream on early disconnects, included a new commit in this PR that fixes it.

    Updated the description expanding more on this issue.

  34. xyzconstant force-pushed on Jul 13, 2026
  35. xyzconstant force-pushed on Jul 13, 2026
  36. xyzconstant force-pushed on Jul 13, 2026
  37. xyzconstant force-pushed on Jul 13, 2026
  38. xyzconstant force-pushed on Jul 13, 2026
  39. xyzconstant commented at 6:38 PM on July 13, 2026: contributor

    Added the FooInit test interface which declares construct() so we could trigger that call against a disconnected socket in tests. It exposed a Connection leak, which, for instance, prevented the EventLoop::loop() from ever exiting.

    Updated the description with more details about it and included a fix in the second commit.

  40. ryanofsky commented at 10:48 PM on July 13, 2026: collaborator

    Code review 96c32f39b8d9fac2c6b36ffea98d8ff2235112e6

    Nice tests and fixes! The changes here look pretty good. I would just suggest a number of things to make this easier to understand and review:

    • Since this PR is not only adding test coverage, would change title to something like "Fix error handling when creating clients" and give a description like "Avoid use-after-free if socket is disconnected before ConnectStream connects, and avoid leaks and hangs if client construct calls throw. Also add tests to cover these and other client connection errors"

    • Would suggest adding the tests before making the bugfixes, otherwise it's not clear how the tests and bugfixes relate. It looks like both bugs are caught by the "Passing a disconnected socket will throw" test which is a very short test, and none of the other tests depend on fixes. So it would be clearer to add all the new tests except "Passing a disconnected socket will throw" initially, to be clear they are unrelated to the bugs.

    • PR description is very long because it goes into detail about the history of the bugs and you how debugged them. I think this information is useful and interesting but it makes it harder to get a quick understanding, so I'd suggest moving this detail into separate github issues describing each bug and adding "Fixes #<issue number>" to the relevant fix commits so they will be closed when this PR is merged.

    • It would be good if description of the bugs mentioned they are not new and have always been present. I found current description of the bug in 91f015eb71fbc03773f02d6177cb2d099a4bef4b that begins with "Previously," confusing because I thought it was referring to something that was previously working but now broken, when actually it's fixing something that has always been broken.

    • I think it might be clearer to fix both bugs and add the short "Passing a disconnected socket will throw" all in a single commit. The first fix 91f015eb71fbc03773f02d6177cb2d099a4bef4b looks correct but it would seem nicer if it just let ProxyClientBase destructor be fully responsible for destroying the connection when destroy_connection is true instead of requiring the caller to destroy it. So could add something like if (destroy_connection) { sync([&]{ connection->onDisconnect(...); } } the end of the ProxyClientBase constructor to make ConnectStream simpler than it was before, instead of more complicated.

    • I think it would be good to add a comment to ConnectStream that it calls the InitInterface.construct method if one is present, so it may block or throw.

  41. xyzconstant force-pushed on Jul 14, 2026
  42. xyzconstant force-pushed on Jul 14, 2026
  43. xyzconstant force-pushed on Jul 14, 2026
  44. xyzconstant renamed this:
    Add test coverage for `ConnectStream`
    Fix error handling when creating clients
    on Jul 15, 2026
  45. xyzconstant renamed this:
    Fix error handling when creating clients
    Fix error handling when creating clients (`mp::ConnectStream`)
    on Jul 15, 2026
  46. xyzconstant force-pushed on Jul 15, 2026
  47. xyzconstant force-pushed on Jul 15, 2026
  48. xyzconstant force-pushed on Jul 15, 2026
  49. xyzconstant force-pushed on Jul 15, 2026
  50. xyzconstant commented at 5:50 PM on July 15, 2026: contributor

    re: #298 (comment)

    Thanks for the review @ryanofsky!

    Addressed your feedback and force-pushed, I hope the commit history is cleaner now.

    Opened issues (#308 and #309) and updated the PR title and description as well.

    Also, please note that I've added 2 new tests (I just noticed I pushed them right after your latest review, so you might have missed them), and I think they're valuable for showcasing the difference with an Init interface without construct(). They're included in the latest commit along with the single "disconnected socket" case.

  51. xyzconstant force-pushed on Jul 15, 2026
  52. DrahtBot added the label Needs rebase on Jul 17, 2026
  53. xyzconstant force-pushed on Jul 21, 2026
  54. DrahtBot removed the label Needs rebase on Jul 21, 2026
  55. xyzconstant force-pushed on Jul 21, 2026
  56. xyzconstant force-pushed on Jul 21, 2026
  57. xyzconstant commented at 8:27 PM on July 21, 2026: contributor

    Rebased on master which now includes #274. Adapted the tests to the stream API (MakeStream) and TestSetup now holds an EventLoopRef because it needs it to keep loop() running.

  58. in test/mp/test/foo.capnp:42 in 31c1ac2203 outdated
      38 | @@ -39,6 +39,10 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
      39 |      passDataPointers @22 (arg :List(Data)) -> (result :List(Data));
      40 |  }
      41 |  
      42 | +interface FooInit $Proxy.wrap("mp::test::FooInit") {
    


    ryanofsky commented at 3:05 AM on July 30, 2026:

    In commit "Add test coverage for ConnectStream" (31c1ac2203b4f5ea3c68fc26672b93fcaae2c9a9)

    Am curious why this new FooInit interface is needed and existing Foo interface isn't used.

    It also seems like a potentially complicating factor that could make the tests harder to debug & understand for this to have a construct method. I wonder if it could be dropped or at least the Thread map parameters could be dropped since it doesn't look like anything in these tests requires threadmaps

    EDIT: Oh, I see in next commit it looks like there are new tests that rely on the construct call failing. I think it would be to only use the FooInit type for the tests which actually need the construct method, and use FooInterface for other tests. Also would be good to drop ThreadMap parameters as I believe they should not be needed.


    xyzconstant commented at 11:54 PM on August 4, 2026:

    Nice suggestion!

    I addressed it in 44d191420c6fdc83d2c1da159a409c4c18151cbe by removing the ThreadMap parameters.


    xyzconstant commented at 12:00 AM on August 5, 2026:

    Regarding the FooInit/FooInterface split, the tests already follow this. Only the 4 tests that require the construct() call use FooInit.

    Also, I've replaced the initThreadMap call with a simple add(1, 2) in the "ConnectStream defers disconnect failure to the first IPC request for interfaces without construct()" test case.

  59. ryanofsky commented at 3:21 AM on July 30, 2026: collaborator

    Code review f3355b5e501a384aa335c58f6309432a8f900dd2. I need to take more time to understand the tests but the bugfix looks right and useful for improving the stability of the C++ IPC client if it connects to a server that disconnects right away.

    I left a minor suggestion below. Also would note that this conflicts with #231 and while I think conflicts mostly just come from moved code, it could be useful to try merging the two PRs and making sure the new tests do not introduce any unix-isms.

  60. ryanofsky referenced this in commit c437d7f107 on Aug 3, 2026
  61. xyzconstant force-pushed on Aug 4, 2026
  62. xyzconstant force-pushed on Aug 4, 2026
  63. xyzconstant force-pushed on Aug 4, 2026
  64. Extract `UnixListener` class to a dedicated file
    A later commit will consume the `UnixListener` class in another test file,
    so move it to a shared one.
    060c1a50d0
  65. Correct stale UnixListener doc comment 231361ae5a
  66. Add test coverage for ConnectStream
    This commit introduces a new test file `connect_tests.cpp`
    for testing the `ConnectStream` function. It also adds a `FooInit`
    test interface declaring a `construct()` method, which
    `ConnectStream` calls implicitly when present.
    44d191420c
  67. Fix error handling when creating clients
    Two bugs that have always been present:
    
    1. `ConnectStream` registered the `onDisconnect` handler that deletes the
    `Connection` before the `ProxyClient` object owning it was created, so an
    early disconnect could delete the `Connection` while the client constructor
    was reading it (#308).
    
    2. A `construct()` method failing during client construction caused a
    `Connection` leak. Normally, cleanup happens in the destructor but a
    constructor that throws leaves no object behind, so it never runs, resulting
    in an event loop ref preventing `EventLoop::loop()` from exiting (#309).
    
    Fix the first by making `ProxyClientBase` responsible for the connection
    when `destroy_connection` is true, registering the delete-on-disconnect
    handler at the end of its constructor. Fix the second by running the cleanup
    functions before rethrowing.
    
    Fixes #308
    Fixes #309
    bb473690c9
  68. xyzconstant commented at 11:54 PM on August 4, 2026: contributor

    Rebased with master and fixed surfaced IWYU issues.

    Additionally, addressed @ryanofsky's feedback by removing the ThreadMap parameter from the (new) FooInit's construct method. Thanks for the review!

  69. xyzconstant force-pushed on Aug 5, 2026
  70. example: Remove unused kj/async.h include
    Flagged by IWYU.
    fae9a637e3
  71. in test/mp/test/connect_tests.cpp:42 in 44d191420c
      37 | +}
      38 | +
      39 | +class TestSetup
      40 | +{
      41 | +public:
      42 | +    int client_fd;
    


    ryanofsky commented at 8:08 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    All these class members should have m_ prefixes so it is clear when they are accessed from methods that they are class members and not local variables. Lack of prefixes also makes code confusing below because there are two different variables called loop


  72. in test/mp/test/connect_tests.cpp:33 in 44d191420c
      28 | +namespace test {
      29 | +namespace {
      30 | +
      31 | +//! Default event loop log handler used by tests, throws so the calling code
      32 | +//! can assert on errors.
      33 | +void DefaultLogHandler(mp::LogMessage log)
    


    ryanofsky commented at 8:12 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    This seems to be slightly different than the DefaultLogHandler defined in listen_tests.cpp. Would be nice to define a shared on, maybe in a test.h file.


    xyzconstant commented at 3:51 AM on August 13, 2026:

    Nice suggestion! I moved this logger to a common.h file instead at https://github.com/bitcoin-core/libmultiprocess/pull/344/changes/ae5c6bc8637b79968b2fced5a556e2ca55d8af63 (follow-up PR).

  73. in test/mp/test/connect_tests.cpp:45 in 44d191420c
      40 | +{
      41 | +public:
      42 | +    int client_fd;
      43 | +    int server_fd;
      44 | +
      45 | +    mp::EventLoop* loop;
    


    ryanofsky commented at 8:15 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    Would probably drop mp:: prefix throughout this file since tests are in the mp namespace.


  74. in test/mp/test/connect_tests.cpp:58 in 44d191420c
      53 | +              [](int fds[2]) {
      54 | +                  KJ_REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1);
      55 | +              },
      56 | +              log_handler) {}
      57 | +
      58 | +    TestSetup(const std::function<void(int[2])>& init_sockets,
    


    ryanofsky commented at 8:29 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    Having the init_sockets callback and the client_fd and server_fd members seems unnecessarily complicated given that nothing else in the test setup class uses them. Would seem simpler to drop these

    <details><summary>diff</summary> <p>

    --- a/test/mp/test/connect_tests.cpp
    +++ b/test/mp/test/connect_tests.cpp
    @@ -9,16 +9,15 @@
     #include <kj/test.h>
     #include <mp/proxy.h>
     #include <mp/proxy-io.h>
    +#include <mp/util.h>
     #include <mp/test/foo.capnp.h>
     #include <mp/test/foo.capnp.proxy.h>
     #include <sys/socket.h>
    -#include <sys/types.h>
     #include <unistd.h>
     
     #include <chrono>
     #include <condition_variable>
     #include <cstring> // IWYU pragma: keep
    -#include <functional>
     #include <future>
     #include <memory>
     #include <mutex>
    @@ -45,9 +44,6 @@ void DefaultLogHandler(mp::LogMessage log)
     class TestSetup
     {
     public:
    -    int client_fd;
    -    int server_fd;
    -
         mp::EventLoop* loop;
         std::optional<mp::EventLoopRef> loop_ref;
         //! Thread variable should be after other struct members so the thread does
    @@ -55,14 +51,6 @@ public:
         std::thread loop_thread;
     
         TestSetup(mp::LogFn log_handler = DefaultLogHandler)
    -        : TestSetup(
    -              [](int fds[2]) {
    -                  KJ_REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1);
    -              },
    -              log_handler) {}
    -
    -    TestSetup(const std::function<void(int[2])>& init_sockets,
    -              mp::LogFn log_handler = DefaultLogHandler)
         {
             std::promise<mp::EventLoop*> loop_promise;
             loop_thread = std::thread([&, log_handler] {
    @@ -72,13 +60,6 @@ public:
             });
             loop = loop_promise.get_future().get();
             loop_ref.emplace(*loop);
    -
    -        // Initialize and store sockets
    -        int fds[2] = {-1, -1};
    -        init_sockets(fds);
    -
    -        client_fd = fds[0];
    -        server_fd = fds[1];
         }
     
         ~TestSetup()
    @@ -91,15 +72,16 @@ public:
     KJ_TEST("ConnectStream connects to a socket serving a valid init interface")
     {
         TestSetup setup;
    +    auto [client_fd, server_fd] = SocketPair();
     
    -    std::thread server_thread([&setup]() {
    +    std::thread server_thread([&]() {
             mp::EventLoop server_loop("mptest-valid-server", DefaultLogHandler);
             std::unique_ptr<FooInit> init = std::make_unique<FooInit>();
    -        ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, setup.server_fd), *init);
    +        ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, server_fd), *init);
             server_loop.loop();
         });
     
    -    auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +    auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
         init.reset();
         server_thread.join();
    @@ -109,11 +91,12 @@ KJ_TEST("ConnectStream connects to a socket serving a valid init interface")
     KJ_TEST("ConnectStream throws when the socket is already disconnected")
     {
         TestSetup setup;
    +    auto [client_fd, server_fd] = SocketPair();
     
    -    close(setup.server_fd);
    +    close(server_fd);
     
         try {
    -        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
             KJ_EXPECT(false);
         } catch (const std::runtime_error& e) {
    @@ -126,12 +109,13 @@ KJ_TEST("ConnectStream throws when the socket is already disconnected")
     KJ_TEST("ConnectStream defers disconnect failure to the first IPC request for interfaces without construct()")
     {
         TestSetup setup;
    +    auto [client_fd, server_fd] = SocketPair();
     
    -    close(setup.server_fd);
    +    close(server_fd);
     
         // Without a construct() method no IPC call is made during client
         // creation, so ConnectStream succeeds even though the peer is gone.
    -    auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +    auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
         try {
             foo->add(1, 2);
    @@ -157,10 +141,11 @@ KJ_TEST("ConnectStream handles a disconnect when no client calls are made")
             }
             DefaultLogHandler(log);
         });
    +    auto [client_fd, server_fd] = SocketPair();
     
    -    close(setup.server_fd);
    +    close(server_fd);
     
    -    auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +    auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
         // The disconnect handler registered by ProxyClientBase should run and
         // delete the connection even when no calls are ever made.
    @@ -171,20 +156,21 @@ KJ_TEST("ConnectStream handles a disconnect when no client calls are made")
     KJ_TEST("ConnectStream throws when the socket disconnects after receiving data")
     {
         TestSetup setup;
    +    auto [client_fd, server_fd] = SocketPair();
     
    -    std::thread server_thread([&setup]() {
    +    std::thread server_thread([&]() {
             char buf[128];
     
             ssize_t bytes_received =
    -            recv(setup.server_fd, buf, sizeof(buf), 0);
    +            recv(server_fd, buf, sizeof(buf), 0);
     
             if (bytes_received > 0) {
    -            close(setup.server_fd);
    +            close(server_fd);
             }
         });
     
         try {
    -        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
             if (server_thread.joinable()) server_thread.join();
             KJ_EXPECT(false);
    @@ -199,16 +185,14 @@ KJ_TEST("ConnectStream throws when the socket disconnects after receiving data")
     KJ_TEST("ConnectStream throws when a connection accepted from a listener disconnects after receiving data")
     {
         UnixListener listener;
    +    TestSetup setup;
    +    int client_fd = listener.MakeConnectedSocket();
    +    int server_fd = listener.release();
     
    -    TestSetup setup([&listener](int fds[2]) {
    -        fds[0] = listener.MakeConnectedSocket(); // client_fd
    -        fds[1] = listener.release();             // server_fd
    -    });
    -
    -    std::thread server_thread([&setup]() {
    +    std::thread server_thread([&]() {
             char buf[128];
     
    -        int connection_fd = accept(setup.server_fd, nullptr, nullptr);
    +        int connection_fd = accept(server_fd, nullptr, nullptr);
     
             if (connection_fd >= 0) {
                 ssize_t bytes_received =
    @@ -218,11 +202,11 @@ KJ_TEST("ConnectStream throws when a connection accepted from a listener disconn
                     close(connection_fd);
                 }
             }
    -        close(setup.server_fd);
    +        close(server_fd);
         });
     
         try {
    -        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    +        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
     
             if (server_thread.joinable()) server_thread.join();
             KJ_EXPECT(false);
    

    </p> </details>


    xyzconstant commented at 3:54 AM on August 13, 2026:

    Yeah, you're right, having them was indeed unnecessary. Addressed this at https://github.com/bitcoin-core/libmultiprocess/pull/344/changes/466afd0ecb4fc802ce28eaca7f4b02aad6efe1a3 (follow-up PR).

    Thanks for the diff!

  75. in test/mp/test/connect_tests.cpp:121 in 44d191420c
     116 | +    });
     117 | +
     118 | +    try {
     119 | +        auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
     120 | +
     121 | +        if (server_thread.joinable()) server_thread.join();
    


    ryanofsky commented at 8:33 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    I don't think it makes sense to call joinable here and to repeat this same line. It would make more sense to simply call server_thread.join() unconditionally at the end of this function after the try/catch.

    Same comment also applies to test below


    xyzconstant commented at 3:56 AM on August 13, 2026:
  76. in test/mp/test/connect_tests.cpp:100 in 44d191420c
      95 | +
      96 | +    auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
      97 | +
      98 | +    init.reset();
      99 | +    server_thread.join();
     100 | +    KJ_EXPECT(true);
    


    ryanofsky commented at 8:33 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    Expecting true here seems unnecessary.


  77. in test/mp/test/connect_tests.cpp:114 in 44d191420c
     109 | +
     110 | +        ssize_t bytes_received =
     111 | +            recv(setup.server_fd, buf, sizeof(buf), 0);
     112 | +
     113 | +        if (bytes_received > 0) {
     114 | +            close(setup.server_fd);
    


    ryanofsky commented at 8:35 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    I don't understand the reason for only closing the descriptor if bytes or received (and leaking otherwise)? Would make more sense to close it unconditionally. Same applies to test below. If there is a reason for this conditional it would be good to explain in a comment


    xyzconstant commented at 4:06 AM on August 13, 2026:

    Good suggestion. Honestly, when I wrote this I thought recv() might somewhat "read 0 bytes", but I see now that I had it wrong. There's no reason to have this conditional, so dropped it at https://github.com/bitcoin-core/libmultiprocess/pull/344/changes/29770728c386b8aa700b863babc555d185d8a287 (follow-up PR).

  78. in test/mp/test/connect_tests.cpp:96 in 44d191420c
      91 | +        std::unique_ptr<FooInit> init = std::make_unique<FooInit>();
      92 | +        ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, setup.server_fd), *init);
      93 | +        server_loop.loop();
      94 | +    });
      95 | +
      96 | +    auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
    


    ryanofsky commented at 8:41 PM on August 10, 2026:

    In commit "Add test coverage for ConnectStream" (44d191420c6fdc83d2c1da159a409c4c18151cbe)

    Might be good to note in a comment that FooInit capnproto interface has a construct method, so this is not just connecting to the IPC server, but also testing that the construct IPC request completes successfully.


  79. in test/mp/test/connect_tests.cpp:113 in bb473690c9
     105 | @@ -100,6 +106,68 @@ KJ_TEST("ConnectStream connects to a socket serving a valid init interface")
     106 |      KJ_EXPECT(true);
     107 |  }
     108 |  
     109 | +KJ_TEST("ConnectStream throws when the socket is already disconnected")
     110 | +{
     111 | +    TestSetup setup;
     112 | +
     113 | +    close(setup.server_fd);
    


    ryanofsky commented at 8:44 PM on August 10, 2026:

    In commit "Fix error handling when creating clients" (bb473690c97ceed78a482d6a97b480cec4a63190)

    Would be good to use KJ_SYSCALL here and below to check close return value


  80. in test/mp/test/connect_tests.cpp:233 in bb473690c9
     229 | @@ -162,7 +230,7 @@ KJ_TEST("ConnectStream throws when a connection accepted from a listener disconn
     230 |          if (server_thread.joinable()) server_thread.join();
     231 |  
     232 |          std::string_view reason = e.what();
     233 | -        KJ_EXPECT(reason == "IPC client method called after disconnect.");
     234 | +        KJ_EXPECT(reason == "IPC client method call interrupted by disconnect.");
    


    ryanofsky commented at 9:00 PM on August 10, 2026:

    In commit "Fix error handling when creating clients" (bb473690c97ceed78a482d6a97b480cec4a63190)

    It would be helpful if commit message noted the reason existing tests in this commit are changing, that because the onDisconnect call now happens later, client code might detect disconnects happening during IPC calls instead of before them.


    xyzconstant commented at 4:12 AM on August 13, 2026:

    Sorry, couldn't reword the commit in time. Thanks for the feedback, though.

  81. in test/mp/test/test.cpp:299 in bb473690c9
     295 | +    } catch (const std::runtime_error& e) {
     296 | +        std::string_view reason{e.what()};
     297 | +
     298 | +        // The disconnect handler may delete the connection before the
     299 | +        // call is processed or while the call is in flight, both errors are possible.
     300 | +        KJ_EXPECT(reason == "IPC client method called after disconnect." || reason == "IPC client method call interrupted by disconnect.");
    


    ryanofsky commented at 9:05 PM on August 10, 2026:

    In commit "Fix error handling when creating clients" (bb473690c97ceed78a482d6a97b480cec4a63190)

    I'm confused about this change. I don't understand if disconnects are detected later now, why the "interrupted by disconnect" message would change to an "interrupted after connect". Would be good to have a more specific comment explaining this, or possibly reverting the change here if not necessary.

    EDIT: Reason for this changes seems to be that test.cpp does not use ConnectStream so no disconnect handler was ever registered previously, and now one is registed since the disconnect handler was moved to ProxyClientBase. Would be helpful if commit message mentioned that as reason this test is changing.

  82. in test/mp/test/connect_tests.cpp:168 in bb473690c9
     163 | +    auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
     164 | +
     165 | +    // The disconnect handler registered by ProxyClientBase should run and
     166 | +    // delete the connection even when no calls are ever made.
     167 | +    std::unique_lock<std::mutex> lock(mutex);
     168 | +    KJ_EXPECT(cv.wait_for(lock, FAILURE_TIMEOUT, [&] { return warned; }));
    


    ryanofsky commented at 9:18 PM on August 10, 2026:

    In commit "Fix error handling when creating clients" (bb473690c97ceed78a482d6a97b480cec4a63190)

    Note: technically it would be possible to simplify this test by having it wait for the client connection pointer to be null, instead of waiting for the "unexpected network disconnect" log message. But that approach would stop working after #336 which changes connections to use shared_ptr and no longer track lists of clients client objects. So it makes sense to keep the current approach.

  83. ryanofsky approved
  84. ryanofsky commented at 9:21 PM on August 10, 2026: collaborator

    Code review ACK fae9a637e35df7020c5f42677e9f089754a95d2c. I left a lot of comments here about the tests, but the fix itself looks very good and this could be merged as-is.

  85. ryanofsky merged this on Aug 11, 2026
  86. ryanofsky closed this on Aug 11, 2026

  87. xyzconstant commented at 4:13 AM on August 13, 2026: contributor

    Thanks for the review @ryanofsky!

    Unfortunately, I couldn't address your feedback in time before this PR was merged. Opened a follow-up PR to address your suggestions: #344.

    I replied to some of your inline comments with links to the commit in the follow-up PR that tackles it.


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-08-26 01:30 UTC

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