Skip to content

fix(netlink): request unique port identifiers - #910

Open
stephenlclarke wants to merge 3 commits into
apple:mainfrom
stephenlclarke:upstream/netlink-port-identifiers
Open

stephenlclarke wants to merge 3 commits into
apple:mainfrom
stephenlclarke:upstream/netlink-port-identifiers

Conversation

@stephenlclarke

@stephenlclarke stephenlclarke commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Allow multiple Netlink route sockets in one process by requesting kernel-assigned port identifiers. Binding every socket with nl_pid = getpid() makes a second concurrent socket collide with the first. Bind with identifier zero and read the assigned identifier using getsockname.

Close the descriptor on every post-socket() initialization failure. Preserve actual errno for failed socket-name syscalls. Following Jon Logan's review, report distinct errors for invalid returned address length, address family, and zero port identifier, with expected and actual values where applicable.

The address validation helper is shared by the Linux initializer and portable unit tests, so the validation branches can also be exercised on macOS.

Buffer pointer lifetime

The shared [UInt8].bind helper returned a pointer after its withUnsafeMutableBytes closure had ended. That pointer was no longer guaranteed to be valid, even while the array remained alive. Both the socket initializer and Netlink error parser used it.

Remove this package-scoped helper and migrate both callers. The socket initializer now runs bind and getsockname within one mutable-byte access closure, then validates the copied address after the closure returns. The error parser uses copyOut(as: Int32.self) to obtain a value and the next offset. Source comments record the lifetime requirement to prevent reintroducing an escaping pointer.

Remove tests of the deleted pointer API, retain value-copy bounds tests and successful ACK tests, and add negative NLMSG_ERROR and NLMSG_DONE response coverage. Pointer lifetime itself is enforced by the scoped call structure and reviewed statically; the unit tests validate behavior.

Testing

  • Passed 57 tests across the Netlink and buffer suites using make test 'FILTER=ContainerizationNetlinkTests|BufferTest' with warnings treated as errors, coverage enabled, and SWIFT_CONFIGURATION_EXTRA='--build-system native --jobs 6'. This includes both negative error-response formats, existing successful ACKs, and buffer bounds tests.
  • Added cases for short, zero, and oversized returned address lengths, incorrect family, zero identifier, valid identifiers at both bounds, and syscall error descriptions. Coverage records execution of every region in the new validator and all new diagnostic descriptions.
  • Regression proof: temporarily restoring the generic EINVAL errors caused the three diagnostic tests to fail across five parameter cases. Restored the fix and reran the Netlink suite successfully.
  • Passed all 58 focused tests on native ARM64 Linux in the official swift:6.4.1-noble image at commit 52c0f9df90a99b8e83b698e6d0754120abec80a5, with repository build prerequisites installed, warnings treated as errors, and coverage enabled. This includes the Linux-only regression opening eight simultaneous real route sockets and checking nonzero, distinct assigned identifiers.
  • Successfully cross-compiled the production ContainerizationNetlink target for aarch64-swift-linux-musl using Swift 6.4.1 and its matching Static Linux SDK with warnings treated as errors.
  • The earlier Static Linux SDK test build could not find Testing. Native Linux testing is now complete using the full toolchain in the official image, which supplies the module without an extra package dependency. Image digest: sha256:19cbe56bdaec1918f7cf5fdeba42b9bd55bdd8a6149c9990d0e0e3a89a127d63.
  • Focused strict Swift formatting and git diff --check pass.
  • An independent review of the entire PR and final pointer-lifetime fix found no additional actionable bugs. Syscall failure cleanup was inspected statically; failure injection is not covered by these tests.

Compatibility

The public property remains pid, but represents a Netlink socket port identifier and is not necessarily the process ID. New public error enum cases can require updates to exhaustive downstream switches.

The package-scoped pointer-returning [UInt8].bind helper has been removed. Its two production callers now keep socket syscall pointers inside withUnsafeMutableBytes and copy out error-code values. This also removes the pre-existing pointer-lifetime hazard. The helper was package-scoped, and no usages remain in the package.

@stephenlclarke
stephenlclarke force-pushed the upstream/netlink-port-identifiers branch from 2ee4d23 to ed533ab Compare September 28, 2026 08:03
guard let ptr = buffer.bind(as: sockaddr.self, size: buffer.count) else {
throw NetlinkSocketError.bindFailure(rc: 0)
}
guard osBind(socketFD, ptr, UInt32(buffer.count)) >= 0 else {

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.

I think we should use socklen_t throughout if you can.

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.

Updated in d35cb51acce19b7df5ea5d0cf75e88e88a303331 and rebased onto current upstream. Both the bind buffer length and returned-address length comparison now use socklen_t; only the kernel netlink port identifier remains UInt32. The current ContainerizationNetlink production target cross-compiles successfully for aarch64 Linux/musl with Swift 6.3 and warnings as errors. This is compilation proof; I have not relabelled the earlier VM test failures as passes.

@stephenlclarke
stephenlclarke force-pushed the upstream/netlink-port-identifiers branch from ed533ab to d35cb51 Compare October 19, 2026 07:28

@jglogan jglogan 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.

@stephenlclarke just one question on error handling.

}

var addrLength = socklen_t(buffer.count)
guard osGetsockname(socketFD, ptr, &addrLength) >= 0 else {

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: A length, family or zero-pid failure might all report socketNameFailure(rc: EINVAL). Should we distinguish these by error message?

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.

Thanks Jon, agreed. Using socketNameFailure(rc: EINVAL) for all three checks hides which part of the returned address was invalid.

I’ll keep socketNameFailure with the actual errno for a failed getsockname() call, and distinguish unexpected address length, incorrect address family, and a zero port identifier with specific error descriptions. I’ll include the returned values where useful and add focused tests for those diagnostics.

@stephenlclarke stephenlclarke Oct 21, 2026 •

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.

Implemented in f119fc1: failed getsockname calls retain their actual errno, while invalid address length, address family, and zero port identifier now have distinct errors and descriptions. Focused tests cover each diagnostic, including expected and actual values.

The full review also identified a pre-existing pointer-lifetime issue in the shared byte-buffer helper. Addressed in 52c0f9d by removing the pointer-returning helper, keeping bind and getsockname inside withUnsafeMutableBytes, and parsing error codes by value. The PR description and source comments document the reason and both caller changes.

Tests:

  • macOS: all 57 Netlink and buffer tests passed.
  • ARM64 Linux: all 58 Netlink and buffer tests passed using the official swift:6.4.1-noble image. The extra Linux test opens eight Netlink sockets at the same time and checks that each gets a different, nonzero ID.
  • The Netlink library also cross-compiles for ARM64 Linux with Swift 6.4.1 and the matching Static Linux SDK.

I also put the old generic errors back to check that the new tests catch the problem. The three error tests failed as expected. I restored the fix and ran the tests again. They all passed.

The official Linux image includes the Testing module, so the Linux tests now build and run. No extra package was needed.

@stephenlclarke
stephenlclarke force-pushed the upstream/netlink-port-identifiers branch from d35cb51 to c4b4609 Compare October 21, 2026 17:30
Remove the package-scoped byte-array bind helper because its pointer escaped the withUnsafeMutableBytes access. Keep bind and getsockname inside that access and parse Netlink error codes as copied values.

Retain existing ACK and buffer bounds tests and cover negative NLMSG_ERROR and NLMSG_DONE replies. Document the pointer-lifetime invariant beside the shared helpers and socket calls.
@stephenlclarke
stephenlclarke force-pushed the upstream/netlink-port-identifiers branch from 52c0f9d to 9320c9c Compare October 28, 2026 11:37

This branch has not been deployed

No deployments
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.

3 participants