diff --git a/Sources/ContainerizationExtras/UInt8+DataBinding.swift b/Sources/ContainerizationExtras/UInt8+DataBinding.swift index 40abedd52..862d8c966 100644 --- a/Sources/ContainerizationExtras/UInt8+DataBinding.swift +++ b/Sources/ContainerizationExtras/UInt8+DataBinding.swift @@ -42,19 +42,13 @@ extension ArraySlice { } } +// Array storage pointers are valid only inside their unsafe-byte access closure. +// These helpers return copied values or offsets, never borrowed pointers. extension [UInt8] { package func hexEncodedString() -> String { self.map { String(format: "%02hhx", $0) }.joined() } - package mutating func bind(as type: T.Type, offset: Int = 0, size: Int? = nil) -> UnsafeMutablePointer? { - guard self.count >= (size ?? MemoryLayout.size) + offset else { - return nil - } - - return self.withUnsafeMutableBytes { $0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self) } - } - package mutating func copyIn(as type: T.Type, value: T, offset: Int = 0, size: Int? = nil) -> Int? { let size = size ?? MemoryLayout.size guard self.count >= size + offset else { diff --git a/Sources/ContainerizationNetlink/NetlinkSession.swift b/Sources/ContainerizationNetlink/NetlinkSession.swift index c6438ad13..18c33c214 100644 --- a/Sources/ContainerizationNetlink/NetlinkSession.swift +++ b/Sources/ContainerizationNetlink/NetlinkSession.swift @@ -931,14 +931,13 @@ public struct NetlinkSession { } private func parseErrorCode(buffer: inout [UInt8], offset: Int) throws -> (Int32, Int) { - guard let errorPtr = buffer.bind(as: Int32.self, offset: offset) else { + guard let (nextOffset, rc) = buffer.copyOut(as: Int32.self, offset: offset) else { throw BindError.recvMarshalFailure(type: "NetlinkErrorMessage", field: "error") } - let rc = errorPtr.pointee log.trace("RECV-ERR-CODE: \(rc)") - return (rc, offset + MemoryLayout.size) + return (rc, nextOffset) } private func parseErrorResponse(buffer: inout [UInt8], offset: Int) throws -> Int { diff --git a/Sources/ContainerizationNetlink/NetlinkSocket.swift b/Sources/ContainerizationNetlink/NetlinkSocket.swift index 7851ce80b..f0918bef8 100644 --- a/Sources/ContainerizationNetlink/NetlinkSocket.swift +++ b/Sources/ContainerizationNetlink/NetlinkSocket.swift @@ -14,6 +14,14 @@ // limitations under the License. //===----------------------------------------------------------------------===// +#if canImport(Musl) +import Musl +#elseif canImport(Glibc) +import Glibc +#else +import Darwin +#endif + /// A protocol for interacting with a netlink socket. public protocol NetlinkSocket { var pid: UInt32 { get } @@ -28,6 +36,10 @@ public typealias NetlinkSocketProvider = () throws -> any NetlinkSocket public enum NetlinkSocketError: Swift.Error, CustomStringConvertible, Equatable { case socketFailure(rc: Int32) case bindFailure(rc: Int32) + case socketNameFailure(rc: Int32) + case invalidSocketNameLength(expected: socklen_t, actual: socklen_t) + case invalidSocketNameFamily(expected: UInt16, actual: UInt16) + case zeroSocketPortIdentifier case sendFailure(rc: Int32) case recvFailure(rc: Int32) case notImplemented @@ -39,6 +51,14 @@ public enum NetlinkSocketError: Swift.Error, CustomStringConvertible, Equatable return "could not create netlink socket, rc = \(rc)" case .bindFailure(let rc): return "could not bind netlink socket, rc = \(rc)" + case .socketNameFailure(let rc): + return "could not get netlink socket name, rc = \(rc)" + case .invalidSocketNameLength(let expected, let actual): + return "invalid netlink socket address length, expected \(expected) bytes, got \(actual)" + case .invalidSocketNameFamily(let expected, let actual): + return "invalid netlink socket address family, expected \(expected), got \(actual)" + case .zeroSocketPortIdentifier: + return "invalid netlink socket port identifier, expected a nonzero value, got 0" case .sendFailure(let rc): return "could not send netlink packet, rc = \(rc)" case .recvFailure(let rc): @@ -49,17 +69,36 @@ public enum NetlinkSocketError: Swift.Error, CustomStringConvertible, Equatable } } +extension SockaddrNetlink { + /// Validates the address returned by getsockname before accepting its assigned port identifier. + static func boundPortIdentifier(buffer: inout [UInt8], length: socklen_t) throws -> UInt32 { + guard length == socklen_t(buffer.count) else { + throw NetlinkSocketError.invalidSocketNameLength(expected: socklen_t(buffer.count), actual: length) + } + + var address = SockaddrNetlink() + _ = try address.bindBuffer(&buffer, offset: 0) + guard address.family == AddressFamily.AF_NETLINK else { + throw NetlinkSocketError.invalidSocketNameFamily(expected: AddressFamily.AF_NETLINK, actual: address.family) + } + guard address.pid != 0 else { + throw NetlinkSocketError.zeroSocketPortIdentifier + } + return address.pid + } +} + #if os(Linux) #if canImport(Musl) -import Musl let osSocket = Musl.socket let osBind = Musl.bind +let osGetsockname = Musl.getsockname let osSend = Musl.send let osRecv = Musl.recv #elseif canImport(Glibc) -import Glibc let osSocket = Glibc.socket let osBind = Glibc.bind +let osGetsockname = Glibc.getsockname let osSend = Glibc.send let osRecv = Glibc.recv #endif @@ -68,25 +107,40 @@ let osRecv = Glibc.recv public class DefaultNetlinkSocket: NetlinkSocket { private let sockfd: Int32 - /// The process identifier of the process creating this socket. + /// The netlink port identifier assigned to this socket. public let pid: UInt32 /// Creates a new instance. public init() throws { - pid = UInt32(getpid()) - sockfd = osSocket(Int32(AddressFamily.AF_NETLINK), SocketType.SOCK_RAW, NetlinkProtocol.NETLINK_ROUTE) - guard sockfd >= 0 else { + let socketFD = osSocket(Int32(AddressFamily.AF_NETLINK), SocketType.SOCK_RAW, NetlinkProtocol.NETLINK_ROUTE) + guard socketFD >= 0 else { throw NetlinkSocketError.socketFailure(rc: errno) } - let addr = SockaddrNetlink(family: AddressFamily.AF_NETLINK, pid: pid) - var buffer = [UInt8](repeating: 0, count: SockaddrNetlink.size) - _ = try addr.appendBuffer(&buffer, offset: 0) - guard let ptr = buffer.bind(as: sockaddr.self, size: buffer.count) else { - throw NetlinkSocketError.bindFailure(rc: 0) - } - guard osBind(sockfd, ptr, UInt32(buffer.count)) >= 0 else { - throw NetlinkSocketError.bindFailure(rc: errno) + do { + let addr = SockaddrNetlink(family: AddressFamily.AF_NETLINK) + var buffer = [UInt8](repeating: 0, count: SockaddrNetlink.size) + _ = try addr.appendBuffer(&buffer, offset: 0) + var addrLength = socklen_t(buffer.count) + // Both syscalls must finish while the buffer pointer is valid. + try buffer.withUnsafeMutableBytes { bytes in + guard let ptr = bytes.baseAddress?.assumingMemoryBound(to: sockaddr.self) else { + throw NetlinkSocketError.bindFailure(rc: 0) + } + guard osBind(socketFD, ptr, socklen_t(bytes.count)) >= 0 else { + throw NetlinkSocketError.bindFailure(rc: errno) + } + guard osGetsockname(socketFD, ptr, &addrLength) >= 0 else { + throw NetlinkSocketError.socketNameFailure(rc: errno) + } + } + let portIdentifier = try SockaddrNetlink.boundPortIdentifier(buffer: &buffer, length: addrLength) + + sockfd = socketFD + pid = portIdentifier + } catch { + close(socketFD) + throw error } } diff --git a/Tests/ContainerizationExtrasTests/UInt8+DataBindingTest.swift b/Tests/ContainerizationExtrasTests/UInt8+DataBindingTest.swift index fabc7bf08..dea2ead59 100644 --- a/Tests/ContainerizationExtrasTests/UInt8+DataBindingTest.swift +++ b/Tests/ContainerizationExtrasTests/UInt8+DataBindingTest.swift @@ -59,72 +59,6 @@ struct BufferTest { #expect(slice.hexEncodedString() == "aabbcc") } - // MARK: - bind Tests - - @Test func testBufferBind() throws { - let expectedValue: UInt64 = 0x0102_0304_0506_0708 - let expectedBuffer: [UInt8] = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, - ] - var buffer = [UInt8](repeating: 0, count: 3 * MemoryLayout.size) - guard let ptr = buffer.bind(as: UInt64.self, offset: 2 * MemoryLayout.size) else { - #expect(Bool(false), "could not bind value to buffer") - return - } - - ptr.pointee = expectedValue - #expect(buffer == expectedBuffer) - } - - @Test func testBufferBindZeroOffset() { - let expectedValue: UInt32 = 0x1234_5678 - var buffer = [UInt8](repeating: 0, count: 8) - guard let ptr = buffer.bind(as: UInt32.self, offset: 0) else { - #expect(Bool(false), "could not bind value to buffer at offset 0") - return - } - - ptr.pointee = expectedValue - #expect(buffer[0] == 0x78) - #expect(buffer[1] == 0x56) - #expect(buffer[2] == 0x34) - #expect(buffer[3] == 0x12) - } - - @Test func testBufferBindRangeError() throws { - var buffer = [UInt8](repeating: 0, count: 3 * MemoryLayout.size) - #expect(buffer.bind(as: UInt64.self, offset: 2 * MemoryLayout.size + 1) == nil) - } - - @Test func testBufferBindRangeErrorExactBoundary() { - var buffer = [UInt8](repeating: 0, count: 8) - // Trying to bind UInt64 at offset 1 requires 9 bytes total - #expect(buffer.bind(as: UInt64.self, offset: 1) == nil) - } - - @Test func testBufferBindWithCustomSize() { - var buffer = [UInt8](repeating: 0, count: 16) - // Request a size larger than the type - guard let ptr = buffer.bind(as: UInt32.self, offset: 4, size: 8) else { - #expect(Bool(false), "could not bind with custom size") - return - } - - ptr.pointee = 0xAABB_CCDD - #expect(buffer[4] == 0xDD) - #expect(buffer[5] == 0xCC) - #expect(buffer[6] == 0xBB) - #expect(buffer[7] == 0xAA) - } - - @Test func testBufferBindWithCustomSizeRangeError() { - var buffer = [UInt8](repeating: 0, count: 10) - // Request size 8 at offset 4 would require 12 bytes total - #expect(buffer.bind(as: UInt32.self, offset: 4, size: 8) == nil) - } - // MARK: - copyIn Tests @Test func testCopyInUInt8() { diff --git a/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift b/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift index 6c31a0caa..d2a0911f1 100644 --- a/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift +++ b/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift @@ -21,6 +21,21 @@ import Testing @testable import ContainerizationNetlink struct NetlinkSessionTest { + @Test(arguments: ["2400000002000000", "1400000003000000"]) + func testNegativeErrorCode(messagePrefix: String) throws { + let mockSocket = try MockNetlinkSocket() + // NLMSG_ERROR echoes the request header; NLMSG_DONE carries just the errno. + let echoedHeader = messagePrefix.hasPrefix("24") ? "10000000120001000000000000000000" : "" + mockSocket.responses.append([UInt8](hex: messagePrefix + "0000000000000000" + "ffffffff" + echoedHeader)) + + let session = NetlinkSession(socket: mockSocket) + #expect(throws: NetlinkDataError.responseError(rc: -1)) { + try session.linkGet(interface: "eth0") + } + #expect(mockSocket.requests.count == 1) + #expect(mockSocket.responseIndex == 1) + } + @Test func testNetworkLinkDown() throws { let mockSocket = try MockNetlinkSocket() mockSocket.pid = 0xc00c_c00c diff --git a/Tests/ContainerizationNetlinkTests/NetlinkSocketTest.swift b/Tests/ContainerizationNetlinkTests/NetlinkSocketTest.swift new file mode 100644 index 000000000..bb7ca2662 --- /dev/null +++ b/Tests/ContainerizationNetlinkTests/NetlinkSocketTest.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Testing + +@testable import ContainerizationNetlink + +#if canImport(Musl) +import Musl +#elseif canImport(Glibc) +import Glibc +#else +import Darwin +#endif + +struct NetlinkSocketTest { + @Test(arguments: [socklen_t(0), socklen_t(SockaddrNetlink.size - 1), socklen_t(SockaddrNetlink.size + 1)]) + func rejectsUnexpectedAddressLength(length: socklen_t) throws { + var buffer = try addressBuffer() + let error = NetlinkSocketError.invalidSocketNameLength(expected: 12, actual: length) + #expect(throws: error) { + try SockaddrNetlink.boundPortIdentifier(buffer: &buffer, length: length) + } + #expect(error.description == "invalid netlink socket address length, expected 12 bytes, got \(length)") + } + + @Test func rejectsUnexpectedAddressFamily() throws { + var buffer = try addressBuffer(family: AddressFamily.AF_INET) + let error = NetlinkSocketError.invalidSocketNameFamily(expected: 16, actual: 2) + #expect(throws: error) { + try SockaddrNetlink.boundPortIdentifier(buffer: &buffer, length: 12) + } + #expect(error.description == "invalid netlink socket address family, expected 16, got 2") + } + + @Test func rejectsZeroPortIdentifier() throws { + var buffer = try addressBuffer(pid: 0) + #expect(throws: NetlinkSocketError.zeroSocketPortIdentifier) { + try SockaddrNetlink.boundPortIdentifier(buffer: &buffer, length: 12) + } + #expect( + NetlinkSocketError.zeroSocketPortIdentifier.description + == "invalid netlink socket port identifier, expected a nonzero value, got 0" + ) + } + + @Test(arguments: [UInt32(1), UInt32.max]) + func acceptsAssignedPortIdentifier(pid: UInt32) throws { + var buffer = try addressBuffer(pid: pid) + #expect(try SockaddrNetlink.boundPortIdentifier(buffer: &buffer, length: 12) == pid) + } + + @Test func socketNameFailurePreservesErrno() { + #expect(NetlinkSocketError.socketNameFailure(rc: EBADF).description == "could not get netlink socket name, rc = \(EBADF)") + } + + private func addressBuffer(family: UInt16 = AddressFamily.AF_NETLINK, pid: UInt32 = 42) throws -> [UInt8] { + var buffer = [UInt8](repeating: 0, count: SockaddrNetlink.size) + _ = try SockaddrNetlink(family: family, pid: pid).appendBuffer(&buffer, offset: 0) + return buffer + } + + #if os(Linux) + @Test func concurrentSocketsReceiveDistinctPortIdentifiers() throws { + let sockets = try (0..<8).map { _ in + try DefaultNetlinkSocket() + } + let identifiers = sockets.map(\.pid) + + #expect(identifiers.allSatisfy { $0 != 0 }) + #expect(Set(identifiers).count == sockets.count) + } + #endif +}