mirror of
https://github.com/apple/container.git
synced 2026-09-13 11:15:41 +00:00
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:
@@ -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 */
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user