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 authored and GitHub committed 2025-10-14 15:59:56 -07:00
1 parent 91ef4009a3
commit 8a34763e40
4 files changed
+262

No files matched your search

+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"
}
}
}