Conversation
When a controller's keepalive expires, unlockAndRemove closes it while its message loop is suspended reading its socket. Closing the descriptor under that read drops it from the socket pool's kqueue or epoll set without waking it: - The read, and with it the controller's handle(for:) task, never finished. - The pool kept the descriptor registered, so a controller accepted later and given the same descriptor was never woken (swhitty/FlyingFox#244). - With that fixed, the stale read could instead be woken by the new controller's data. close() now shuts the socket down in both directions instead. That wakes the read with end of file, and the message loop ends as it would for a peer's close. The socket is closed exactly once, by whichever comes last of close() and the message stream ending, so it is never closed under a suspended read. Both steps happen under one lock, so neither can reach a descriptor already closed and reused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE
2 of 3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a FlyingSocks controller's keepalive expires,
unlockAndRemovecallscontroller.close()while the controller's message loop (handle(for:)) is suspended reading its socket.close()closed the descriptor under that read. Closing a descriptor removes it from kqueue or epoll without waking anyone waiting on it:handle(for:)task.Ocp1DeviceEndpointis the FlyingSocks stream endpoint.Changes
Ocp1FlyingSocksStreamController.close()now shuts the socket down withshutdown(2), in both directions, instead of closing it. That wakes the suspended read with end of file, so the message loop ends as it does when the peer closes the connection.The descriptor is closed exactly once, by whichever comes last:
close();A small
Mutex-protected lifetime object tracks this. Theshutdownandclosecalls are both made while it holds the lock, so neither can reach a descriptor that was already closed and given to another socket.deinitcloses it if neither got that far.Windows uses
shutdown(SOCKET, SD_BOTH). Everywhere else it'sSHUT_RDWR.This works with the FlyingFox we resolve (0.27.1), whether or not #244 is merged.
Behaviour change
When a keepalive expires, the message loop now ends.
handle(for:)then callsunlockAndRemovefor the second time, soonControllerExpiryis signalled twice. The Network.framework and IORing controllers already do this, because closing them wakes their pending receive.Test
FlyingSocksControllerCloseTests.testClosingAControllerWhileItReadsEndsItsMessageLoopdoes the following:handle(for:)over asocketpair, on the endpoint's running socket pool.close()while the loop is suspended reading.On macOS, with the fix it passes in 0.23 s. Without it, it fails after 2 s with "closing the controller should end its message loop".
Test plan
swift build --build-testsclean, full suite passes (290 tests, 0 failures)🤖 Generated with Claude Code
https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE