Skip to content

Add file descriptor passing and fix socket teardown races - #24

Merged
colemancda merged 11 commits into
mainfrom
fix/descriptor-reuse-race
Aug 7, 2026
Merged

Add file descriptor passing and fix socket teardown races#24
colemancda merged 11 commits into
mainfrom
fix/descriptor-reuse-race

Conversation

@colemancda

@colemancda colemancda commented Aug 3, 2026

Copy link
Copy Markdown
Member

Adds Unix file descriptor passing (SCM_RIGHTS) and fixes two socket teardown races found while building a pure-Swift D-Bus client on top of this package.

File descriptor passing

CMSG_FIRSTHDR, CMSG_NXTHDR, CMSG_DATA, CMSG_LEN and CMSG_SPACE are macros, so they are unavailable from Swift on every platform. CSocket gains two shims — c_socket_send_descriptors and c_socket_receive_descriptors — wrapping sendmsg/recvmsg with an ancillary buffer. These are the only C shims compiled on Darwin as well as Linux and Android.

On top of them:

  • SocketMessage — received payload, accompanying descriptors, and whether the ancillary buffer was truncated.
  • SocketDescriptor.send(_:fileDescriptors:) and SocketDescriptor.receive(_:maximumDescriptors:) — blocking, Result-returning, matching the existing descriptor API.
  • Socket.write(_:fileDescriptors:) and Socket.receiveMessage(_:maximumDescriptors:) — async wrappers going through AsyncSocketManager, so a descriptor-carrying send waits for writability like any other write.

Two details worth calling out:

  • Descriptors beyond maximumDescriptors are closed rather than leaked. The kernel has already installed them in this process by the time recvmsg returns, so dropping the buffer would leak them.
  • Sending with an empty payload returns EINVAL. A zero-length message can be discarded before its ancillary data is delivered, so the descriptors would vanish silently.

Teardown fixes

AsyncSocketManager keys its state by file descriptor number, and those numbers are reused as soon as they are closed. Two consequences:

  1. Deferred poll results applied to a reused descriptor. A poll result computed for the old socket could be applied to the new one occupying the same number, tearing down a healthy connection. Poll results are now checked against the current SocketState identity before they take effect, and a descriptor missing from the table is skipped instead of trapping.

  2. A never-connected socket polls as POLLHUP. An AF_UNIX stream socket that has neither connected nor listened reports hangup — verifiable outside Swift entirely. Because Socket.init registers with the manager before connect, the monitor could hang up a socket that was about to be used. SocketState now tracks whether the socket was ever established, and hangup is only acted on for established or listening sockets.

Both were reproducible as intermittent ESHUTDOWN on a freshly opened socket under a concurrent test suite.

Tests

Eight tests covering round trips of one and several descriptors, descriptors surviving with payload data, truncation reporting, over-capacity descriptors being closed, and the empty-payload rejection.

Notes

  • CI now also builds Linux ARM64, and the Swift floor is raised to 6.2.
  • The pre-existing test failures on this repo (the force unwrap at NetworkInterface.swift:58, and the fixed-port and fixed-path tests colliding with leftovers from earlier runs) are untouched by this branch and fail the same way on main.

@colemancda
colemancda force-pushed the fix/descriptor-reuse-race branch 2 times, most recently from d35e59b to 00af41b Compare August 7, 2026 01:56
The SCM_RIGHTS shims are needed on Apple platforms too, because the CMSG_*
accessors are macros, and nothing else in the file needs libc: errno now goes
through Errno.current, whose Android branch the Bionic import did not provide.
Adds a native aarch64 job on the ubuntu-24.04-arm runner, and drops the 6.0.3
and 6.1.2 containers now that 6.2 is the minimum.
Matches the Swift 6.2 minimum used by the other workflows.
SOCK_STREAM is imported as an enum by Glibc but as a plain CInt by Darwin, so
the test target failed to build on macOS. SocketType and SocketAddressFamily
already paper over that difference.
@colemancda
colemancda force-pushed the fix/descriptor-reuse-race branch from 2aa3f2e to 3ed3011 Compare August 7, 2026 02:03
@colemancda colemancda changed the title Fix descriptor reuse race Add file descriptor passing and fix socket teardown races Aug 7, 2026
@colemancda
colemancda merged commit c7293f7 into main Aug 7, 2026
37 checks passed
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.

1 participant