Add test coverage for ConnectStream#298
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline and AI policy for information on the review process. ConflictsNo conflicts as of last run. |
ConnectStream
|
Thanks for following up to #183 with these tests. They do seem potentially useful. Here is feedback I'd have:
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.) |
d3390db to
cdada78
Compare
cdada78 to
4f05dbe
Compare
595e258 to
bc85c9a
Compare
ConnectStreamConnectStream
02b300c to
7c24708
Compare
|
Thanks for the feedback @ryanofsky! I've just rebased to master now that #269 has been merged and added a new commit to move Also, I've made changes to the tests (please check the description), keeping the first 2 tests (Notice that I've dropped the
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.
Good, these ideas are great and definitely worth exploring. Happy to tackle them after this. |
|
This PR is now ready for review. I've updated the description as well! |
02f2822 to
bd3c83d
Compare
Previously, the `onDisconnect` handler responsible for deleting the `Connection` was registered before the `ProxyClient<InitInterface>` instance was constructed. If the peer was already disconnected, the event loop could run the handler and delete the `Connection` before the client constructor registered its cleanup callbacks, resulting in the constructor accessing freed memory. To resolve this, register the handler in a second `.sync()` call after the client is constructed.
aa095c1 to
a648c51
Compare
|
The netbsd 9.4 job exposed a use-after-free race condition in |
| }); | ||
| loop = loop_promise.get_future().get(); | ||
|
|
||
| // Initalize and store sockets |
There was a problem hiding this comment.
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
a648c51 to
aee480b
Compare
Updated the description expanding more on this issue. |
`Sub::construct(*this)` can make an IPC call. If it fails while `ProxyClient` is being initialized (e.g. sending a request to a disconnected peer), the exception escapes `ProxyClientBase`'s constructor without cleaning up the connection. Cleanup normally happens in the destructor, but a constructor that throws leaves no object behind, so the destructor never runs. As a result, the Connection object is leaked and its `EventLoopRef` keeps `EventLoop::loop()` from ever exiting, hanging the process. Fix by running the cleanup functions before rethrowing, which deletes the connection when it's owned by the client. If a generated
f524017 to
0145706
Compare
Next commit will consume the `UnixListener` class in another test file, so move it to a shared one.
0145706 to
96c32f3
Compare
|
Added the Updated the description with more details about it and included a fix in the second commit. |
|
Code review 96c32f3 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:
|
2381fcb to
fc5dbb8
Compare
This commit introduces a new test file `connect_tests.cpp` for testing the `ConnectStream` client method.
fc5dbb8 to
8fdb95b
Compare
ref: #183
This PR adds test coverage for the client method
ConnectStream, as suggested by @ryanofsky.The following cases are tested:
construct()method.construct()method and making no calls.accept()) that disconnects after some data arrivesAdditionally, a new
FooInitinterface is added, and theUnixListenerclass introduced in #269 to set up listening tests is extracted to a shared file so the newconnect_tests.cppfile can consume it.Bugs found
The added tests exposed a couple of bugs, whose fixes are included in this PR as the first 2 commits. Details about them are below:
Fix 1: Use-after-free race found in the NetBSD 9.4 job
The NetBSD 9.4 job was crashingin the new test
connect_tests.cpp:94with:Asked Claude to try to reproduce it locally and came up with placing (in
ConnectStream)between the
loop.sync(...)block and the client construction to widen the race window so that the event loop thread wins the race (as it consistently did in the failing job), thereby exposing the issue.The sequence of events was as follows: passing a disconnected socket fd causes the RPC system to hit EOF on the first read. The event loop resumes right after the
loop.sync(...)block, resolvesm_network.onDisconnect(), whose handler (registered inside the same sync block) runsdelete connection_ptr.Connection::~Connectionresets itsEventLoopRef m_loopmember, and the memory is freed.The caller thread then wakes up and constructs the
ProxyClient<InitInterface>, whoseProxyContextconstructor reads the freedConnection:libmultiprocess/src/mp/proxy.cpp
Line 82 in 8412fcd
The fix is included in this PR as the first commit. It moves the
onDisconnecthandler registration to a secondloop.sync(...)call just after the proxy client has been constructed.Fix 2: Connection leak on failed
construct()preventsEventLoop::loop()from exitingWhen a capnp init interface declares a
construct()method (as inexample/init.capnpand Bitcoin Core's Init interface), the generatedProxyClientconstructor calls it automatically, making a blocking IPC call while the client is being constructed insideConnectStream.If that call fails, such as when connecting to a disconnected socket, the exception escapes
ProxyClientBase's constructor. Normally, cleanup occurs in the destructor but if the constructor throws no object is created, and the destructor doesn't run. As a result, cleanup functions are not called, theConnectionobject is leaked, and itsEventLoopRefpreventsEventLoop::loop()from exiting.The second commit addresses this by running cleanup functions before rethrowing, which releases the client capability, unregisters the disconnect callback, and deletes the connection when it is owned by the client.
Alternatively, the
Sub::construct()call could be removed from theProxyClientBaseconstructor and called byConnectStreamafter client creation. I am open to switching to this approach if preferred.