Socket: Add fd receiving (#330)

Via scm_rights. Useful for supporting spawning runc as an OCI runtime as
that's how the pty is passed to the client.
This commit is contained in:
Danny Canter
2025-10-14 15:59:56 -07:00
committed by GitHub
parent 91ef4009a3
commit 8a34763e40
4 changed files with 262 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright © 2025 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.
*/
#ifndef socket_helpers_h
#define socket_helpers_h
#include <sys/socket.h>
#include <stdint.h>
// Helper functions to access CMSG macros from Swift
struct cmsghdr* CZ_CMSG_FIRSTHDR(struct msghdr *msg);
void* CZ_CMSG_DATA(struct cmsghdr *cmsg);
size_t CZ_CMSG_SPACE(size_t length);
size_t CZ_CMSG_LEN(size_t length);
#endif /* socket_helpers_h */
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright © 2025 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.
*/
#include "socket_helpers.h"
struct cmsghdr* CZ_CMSG_FIRSTHDR(struct msghdr *msg) {
return CMSG_FIRSTHDR(msg);
}
void* CZ_CMSG_DATA(struct cmsghdr *cmsg) {
return CMSG_DATA(cmsg);
}
size_t CZ_CMSG_SPACE(size_t length) {
return CMSG_SPACE(length);
}
size_t CZ_CMSG_LEN(size_t length) {
return CMSG_LEN(length);
}
@@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import CShim
import Foundation
import Synchronization
@@ -42,6 +43,7 @@ let sysListen = listen
let sysAccept = accept
let sysConnect = connect
let sysIoctl: @convention(c) (CInt, CUnsignedLong, UnsafeMutableRawPointer) -> CInt = ioctl
let sysRecvmsg = recvmsg
#endif
/// Thread-safe socket wrapper.
@@ -102,6 +104,20 @@ public final class Socket: Sendable {
self.state = Mutex(state)
}
/// Internal initializer for wrapping already-connected file descriptors (e.g., from socketpair)
/// Ideally we just get rid of the state machine in this class. Not sure how much value it provides..
init(fd: Int32, type: SocketType, closeOnDeinit: Bool, connected: Bool) {
_queue = DispatchQueue(label: "com.apple.containerization.socket")
_closeOnDeinit = closeOnDeinit
let state = State(
socketState: connected ? .connected : .created,
handle: FileHandle(fileDescriptor: fd, closeOnDealloc: false),
type: type,
acceptSource: nil
)
self.state = Mutex(state)
}
deinit {
if _closeOnDeinit {
try? close()
@@ -288,6 +304,66 @@ extension Socket {
)
}
/// Receive a file descriptor via SCM_RIGHTS control message.
/// This is commonly used for passing file descriptors between processes via Unix domain sockets.
public func receiveFileDescriptor() throws -> FileHandle {
let handle = try state.withLock { currentState in
guard currentState.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("receiveFileDescriptor")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
var msg = msghdr()
var iov = iovec()
var buf: UInt8 = 0
iov.iov_base = withUnsafeMutablePointer(to: &buf) { UnsafeMutableRawPointer($0) }
iov.iov_len = 1
msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 }
msg.msg_iovlen = 1
var cmsgBuf = [UInt8](repeating: 0, count: Int(CZ_CMSG_SPACE(Int(MemoryLayout<Int32>.size))))
msg.msg_control = withUnsafeMutablePointer(to: &cmsgBuf[0]) { UnsafeMutableRawPointer($0) }
msg.msg_controllen = socklen_t(cmsgBuf.count)
let recvResult = withUnsafeMutablePointer(to: &msg) { msgPtr in
sysRecvmsg(handle.fileDescriptor, msgPtr, 0)
}
guard recvResult >= 0 else {
throw Socket.errnoToError(msg: "recvmsg failed")
}
// Extract file descriptor from control message
let cmsgPtr = withUnsafeMutablePointer(to: &msg) { CZ_CMSG_FIRSTHDR($0) }
guard let cmsg = cmsgPtr else {
throw SocketError.invalidFileDescriptor
}
guard cmsg.pointee.cmsg_level == SOL_SOCKET,
cmsg.pointee.cmsg_type == SCM_RIGHTS
else {
throw SocketError.invalidFileDescriptor
}
guard let dataPtr = CZ_CMSG_DATA(cmsg) else {
throw SocketError.invalidFileDescriptor
}
let fdPtr = dataPtr.assumingMemoryBound(to: Int32.self)
let fd = fdPtr.pointee
guard fd >= 0 else {
throw SocketError.invalidFileDescriptor
}
return FileHandle(fileDescriptor: fd, closeOnDealloc: true)
}
public func read(buffer: inout Data) throws -> Int {
let handle = try state.withLock { currentState in
guard currentState.socketState == .connected else {
@@ -396,6 +472,7 @@ public enum SocketError: Error, Equatable, CustomStringConvertible {
case invalidOperationOnSocket(String)
case missingBaseAddress
case withErrno(_ msg: String, errno: Int32)
case invalidFileDescriptor
public var description: String {
switch self {
@@ -409,6 +486,8 @@ public enum SocketError: Error, Equatable, CustomStringConvertible {
return "socket: missing base address"
case .withErrno(let msg, _):
return "socket: error \(msg)"
case .invalidFileDescriptor:
return "socket: invalid file descriptor received"
}
}
}
@@ -0,0 +1,121 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 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 CShim
import Foundation
import Testing
@testable import ContainerizationOS
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
@Suite("Socket SCM_RIGHTS tests")
final class SocketTests {
/// Helper function to send a file descriptor via SCM_RIGHTS
private func sendFileDescriptor(socket: Socket, fd: Int32) throws {
var msg = msghdr()
var iov = iovec()
var buf: UInt8 = 0
iov.iov_base = withUnsafeMutablePointer(to: &buf) { UnsafeMutableRawPointer($0) }
iov.iov_len = 1
msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 }
msg.msg_iovlen = 1
// Control message buffer for file descriptor
var cmsgBuf = [UInt8](repeating: 0, count: Int(CZ_CMSG_SPACE(Int(MemoryLayout<Int32>.size))))
msg.msg_control = withUnsafeMutablePointer(to: &cmsgBuf[0]) { UnsafeMutableRawPointer($0) }
msg.msg_controllen = socklen_t(cmsgBuf.count)
// Set up control message
let cmsgPtr = withUnsafeMutablePointer(to: &msg) { CZ_CMSG_FIRSTHDR($0) }
guard let cmsg = cmsgPtr else {
throw SocketError.invalidFileDescriptor
}
cmsg.pointee.cmsg_level = SOL_SOCKET
cmsg.pointee.cmsg_type = SCM_RIGHTS
cmsg.pointee.cmsg_len = socklen_t(CZ_CMSG_LEN(Int(MemoryLayout<Int32>.size)))
guard let dataPtr = CZ_CMSG_DATA(cmsg) else {
throw SocketError.invalidFileDescriptor
}
dataPtr.assumingMemoryBound(to: Int32.self).pointee = fd
let sendResult = withUnsafeMutablePointer(to: &msg) { msgPtr in
sendmsg(socket.fileDescriptor, msgPtr, 0)
}
guard sendResult >= 0 else {
throw SocketError.withErrno("sendmsg failed", errno: errno)
}
}
@Test
func testSCMRightsFileDescriptorPassing() throws {
// Create a socketpair for testing
var fds: [Int32] = [0, 0]
let result = socketpair(AF_UNIX, SOCK_STREAM, 0, &fds)
try #require(result == 0, "socketpair should succeed")
defer {
close(fds[0])
close(fds[1])
}
// Use a dummy UnixType since we won't be using it for bind/connect/listen
let socketType = try UnixType(path: "/tmp/dummy")
let sendSocket = Socket(fd: fds[0], type: socketType, closeOnDeinit: false, connected: true)
let recvSocket = Socket(fd: fds[1], type: socketType, closeOnDeinit: false, connected: true)
// Create a temporary file to send its descriptor
let fileManager = FileManager.default
let tempDir = fileManager.uniqueTemporaryDirectory()
defer { try? fileManager.removeItem(at: tempDir) }
let testFilePath = tempDir.appending(path: "test.txt")
let testContent = "Hello, SCM_RIGHTS!"
try testContent.write(to: testFilePath, atomically: true, encoding: .utf8)
let testFileHandle = try FileHandle(forReadingFrom: testFilePath)
defer { try? testFileHandle.close() }
let originalFD = testFileHandle.fileDescriptor
try sendFileDescriptor(socket: sendSocket, fd: originalFD)
let receivedFileHandle = try recvSocket.receiveFileDescriptor()
defer { try? receivedFileHandle.close() }
try #require(receivedFileHandle.fileDescriptor != originalFD, "Received FD should be different")
try #require(receivedFileHandle.fileDescriptor >= 0, "Received FD should be valid")
let data = try receivedFileHandle.readToEnd()
try #require(data != nil, "Should be able to read from received FD")
let receivedContent = String(data: data!, encoding: .utf8)
#expect(receivedContent == testContent, "Content should match original file")
}
}