chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:06:18 +08:00
commit e84fb4a79e
474 changed files with 68321 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
import ContainerizationError
import Foundation
public final class XPCClient: Sendable {
/// The maximum amount of time to wait for a request to a recently
/// registered XPC service. Once a service has launched, XPC
/// requests only have milliseconds of overhead, but in some instances,
/// macOS can take 5 seconds (or considerably longer) to launch a
/// service after it has been registered.
public static let xpcRegistrationTimeout: Duration = .seconds(60)
private nonisolated(unsafe) let connection: xpc_connection_t
private let q: DispatchQueue?
private let service: String
public init(service: String, queue: DispatchQueue? = nil) {
let connection = xpc_connection_create_mach_service(service, queue, 0)
self.connection = connection
self.q = queue
self.service = service
xpc_connection_set_event_handler(connection) { _ in }
xpc_connection_set_target_queue(connection, self.q)
xpc_connection_activate(connection)
}
public init(connection: xpc_connection_t, label: String, queue: DispatchQueue? = nil) {
self.connection = connection
self.q = queue
self.service = label
xpc_connection_set_event_handler(connection) { _ in }
xpc_connection_set_target_queue(connection, self.q)
xpc_connection_activate(connection)
}
deinit {
self.close()
}
}
extension XPCClient {
/// Close the underlying XPC connection.
public func close() {
xpc_connection_cancel(connection)
}
/// Returns the pid of process to which we have a connection.
/// Note: `xpc_connection_get_pid` returns 0 if no activity
/// has taken place on the connection prior to it being called.
public func remotePid() -> pid_t {
xpc_connection_get_pid(self.connection)
}
/// Install a handler that is called whenever the connection receives an XPC error event.
///
/// This replaces the existing (no-op) event handler. Call this before the first
/// `send()` to avoid a disconnect-before-handler race.
///
/// ```swift
/// let client = XPCClient(service: "com.example.myservice")
/// client.setDisconnectHandler {
/// print("service disconnected, cleaning up")
/// }
/// let response = try await client.send(request)
/// ```
public func setDisconnectHandler(_ handler: @Sendable @escaping () -> Void) {
xpc_connection_set_event_handler(connection) { object in
if xpc_get_type(object) == XPC_TYPE_ERROR { handler() }
}
}
/// Create a persistent session backed by this client connection.
///
/// The session installs a disconnect handler at initialisation time, before
/// any messages are sent, ensuring no server-exit event is missed.
public func openSession() -> XPCClientSession {
XPCClientSession(client: self)
}
/// Send the provided message to the service.
@discardableResult
public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage {
try await withThrowingTaskGroup(of: XPCMessage.self, returning: XPCMessage.self) { group in
if let responseTimeout {
group.addTask {
try await Task.sleep(for: responseTimeout)
let route = message.string(key: XPCMessage.routeKey) ?? "nil"
throw ContainerizationError(
.internalError,
message: "XPC timeout for request to \(self.service)/\(route)"
)
}
}
group.addTask {
try await withCheckedThrowingContinuation { cont in
xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in
do {
let message = try self.parseReply(reply)
cont.resume(returning: message)
} catch {
cont.resume(throwing: error)
}
}
}
}
let response = try await group.next()
// once one task has finished, cancel the rest.
group.cancelAll()
// we don't really care about the second error here
// as it's most likely a `CancellationError`.
try? await group.waitForAll()
guard let response else {
throw ContainerizationError(.invalidState, message: "failed to receive XPC response")
}
return response
}
}
private func parseReply(_ reply: xpc_object_t) throws -> XPCMessage {
switch xpc_get_type(reply) {
case XPC_TYPE_ERROR:
var code = ContainerizationError.Code.invalidState
if reply.connectionError {
code = .interrupted
}
throw ContainerizationError(
code,
message: "XPC connection error: \(reply.errorDescription ?? "unknown")"
)
case XPC_TYPE_DICTIONARY:
let message = XPCMessage(object: reply)
// check errors from our protocol
try message.error()
return message
default:
fatalError("unhandled xpc object type: \(xpc_get_type(reply))")
}
}
}
#endif
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
import Synchronization
/// Represents a long-lived connection to an XPC service on the client side.
///
/// Obtain one via `XPCClient.openSession()`. The disconnect handler is
/// installed at initialisation time, before the first `send()`, so there is
/// no window in which a server crash goes undetected.
public final class XPCClientSession: Sendable {
private let client: XPCClient
private let handlers: Mutex<[@Sendable () async -> Void]> = Mutex([])
init(client: XPCClient) {
self.client = client
client.setDisconnectHandler { [weak self] in
guard let self else { return }
let snapshot = self.handlers.withLock { $0 }
Task { for handler in snapshot { await handler() } }
}
}
/// Register a handler to be called when the server disconnects.
public func onDisconnect(_ handler: @Sendable @escaping () async -> Void) {
handlers.withLock { $0.append(handler) }
}
/// Send a message over the persistent connection.
@discardableResult
public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage {
try await client.send(message, responseTimeout: responseTimeout)
}
/// Cancel the underlying connection.
public func close() { client.close() }
}
#endif
+291
View File
@@ -0,0 +1,291 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
import ContainerizationError
import Foundation
/// A message that can be pass across application boundaries via XPC.
public struct XPCMessage: Sendable {
/// Defined message key storing the route value.
public static let routeKey = "com.apple.container.xpc.route"
/// Defined message key storing the error value.
public static let errorKey = "com.apple.container.xpc.error"
// Access to `object` is protected by a lock
private nonisolated(unsafe) let object: xpc_object_t
private let lock = NSLock()
private let isErr: Bool
/// The underlying xpc object that the message wraps.
public var underlying: xpc_object_t {
lock.withLock {
object
}
}
public var isErrorType: Bool { isErr }
public init(object: xpc_object_t) {
self.object = object
self.isErr = xpc_get_type(self.object) == XPC_TYPE_ERROR
}
public init(route: String) {
self.object = xpc_dictionary_create_empty()
self.isErr = false
xpc_dictionary_set_string(self.object, Self.routeKey, route)
}
}
extension XPCMessage {
public static func == (lhs: XPCMessage, rhs: xpc_object_t) -> Bool {
xpc_equal(lhs.underlying, rhs)
}
public func reply() -> XPCMessage {
lock.withLock {
XPCMessage(object: xpc_dictionary_create_reply(object)!)
}
}
public func errorKeyDescription() -> String? {
guard self.isErr,
let xpcErr = lock.withLock({
xpc_dictionary_get_string(
self.object,
XPC_ERROR_KEY_DESCRIPTION
)
})
else {
return nil
}
return String(cString: xpcErr)
}
public func error() throws {
let data = data(key: Self.errorKey)
if let data {
let item = try? JSONDecoder().decode(ContainerXPCError.self, from: data)
precondition(item != nil, "expected to receive a ContainerXPCXPCError")
throw ContainerizationError(item!.code, message: item!.message)
}
}
public func set(error: ContainerizationError) {
var message = error.message
if let cause = error.cause {
message += " (cause: \"\(cause)\")"
}
let serializableError = ContainerXPCError(code: error.code.description, message: message)
let data = try? JSONEncoder().encode(serializableError)
precondition(data != nil)
set(key: Self.errorKey, value: data!)
}
}
struct ContainerXPCError: Codable {
let code: String
let message: String
}
extension XPCMessage {
public func data(key: String) -> Data? {
var length: Int = 0
let bytes = lock.withLock {
xpc_dictionary_get_data(self.object, key, &length)
}
guard let bytes else {
return nil
}
return Data(bytes: bytes, count: length)
}
/// dataNoCopy is similar to data, except the data is not copied
/// to a new buffer. What this means in practice is the second the
/// underlying xpc_object_t gets released by ARC the data will be
/// released as well. This variant should be used when you know the
/// data will be used before the object has no more references.
public func dataNoCopy(key: String) -> Data? {
var length: Int = 0
let bytes = lock.withLock {
xpc_dictionary_get_data(self.object, key, &length)
}
guard let bytes else {
return nil
}
return Data(
bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes),
count: length,
deallocator: .none
)
}
public func set(key: String, value: Data) {
value.withUnsafeBytes { ptr in
if let addr = ptr.baseAddress {
lock.withLock {
xpc_dictionary_set_data(self.object, key, addr, value.count)
}
}
}
}
public func string(key: String) -> String? {
let _id = lock.withLock {
xpc_dictionary_get_string(self.object, key)
}
if let _id {
return String(cString: _id)
}
return nil
}
public func set(key: String, value: String) {
lock.withLock {
xpc_dictionary_set_string(self.object, key, value)
}
}
public func bool(key: String) -> Bool {
lock.withLock {
xpc_dictionary_get_bool(self.object, key)
}
}
public func set(key: String, value: Bool) {
lock.withLock {
xpc_dictionary_set_bool(self.object, key, value)
}
}
public func uint64(key: String) -> UInt64 {
lock.withLock {
xpc_dictionary_get_uint64(self.object, key)
}
}
public func set(key: String, value: UInt64) {
lock.withLock {
xpc_dictionary_set_uint64(self.object, key, value)
}
}
public func int64(key: String) -> Int64 {
lock.withLock {
xpc_dictionary_get_int64(self.object, key)
}
}
public func set(key: String, value: Int64) {
lock.withLock {
xpc_dictionary_set_int64(self.object, key, value)
}
}
public func date(key: String) -> Date {
lock.withLock {
let nsSinceEpoch = xpc_dictionary_get_date(self.object, key)
return Date(timeIntervalSince1970: TimeInterval(nsSinceEpoch) / 1_000_000_000)
}
}
public func set(key: String, value: Date) {
lock.withLock {
let nsSinceEpoch = Int64(value.timeIntervalSince1970 * 1_000_000_000)
xpc_dictionary_set_date(self.object, key, nsSinceEpoch)
}
}
public func fileHandle(key: String) -> FileHandle? {
let fd = lock.withLock {
xpc_dictionary_get_value(self.object, key)
}
if let fd {
let fd2 = xpc_fd_dup(fd)
return FileHandle(fileDescriptor: fd2, closeOnDealloc: false)
}
return nil
}
public func set(key: String, value: FileHandle) {
let fd = xpc_fd_create(value.fileDescriptor)
close(value.fileDescriptor)
lock.withLock {
xpc_dictionary_set_value(self.object, key, fd)
}
}
public func fileHandles(key: String) -> [FileHandle]? {
let fds = lock.withLock {
xpc_dictionary_get_value(self.object, key)
}
if let fds {
let fd1 = xpc_array_dup_fd(fds, 0)
let fd2 = xpc_array_dup_fd(fds, 1)
if fd1 == -1 || fd2 == -1 {
return nil
}
return [
FileHandle(fileDescriptor: fd1, closeOnDealloc: false),
FileHandle(fileDescriptor: fd2, closeOnDealloc: false),
]
}
return nil
}
public func set(key: String, value: [FileHandle]) throws {
let fdArray = xpc_array_create(nil, 0)
for fh in value {
guard let xpcFd = xpc_fd_create(fh.fileDescriptor) else {
throw ContainerizationError(
.internalError,
message: "failed to create xpc fd for \(fh.fileDescriptor)"
)
}
xpc_array_append_value(fdArray, xpcFd)
close(fh.fileDescriptor)
}
lock.withLock {
xpc_dictionary_set_value(self.object, key, fdArray)
}
}
public func set(key: String, xpcDictionary: xpc_object_t) {
lock.withLock {
xpc_dictionary_set_value(self.object, key, xpcDictionary)
}
}
public func endpoint(key: String) -> xpc_endpoint_t? {
lock.withLock {
xpc_dictionary_get_value(self.object, key)
}
}
public func set(key: String, value: xpc_endpoint_t) {
lock.withLock {
xpc_dictionary_set_value(self.object, key, value)
}
}
}
#endif
+288
View File
@@ -0,0 +1,288 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
import CAuditToken
import ContainerizationError
import Foundation
import Logging
import os
import Synchronization
public struct XPCServer: Sendable {
public typealias RouteHandler = @Sendable (XPCMessage, XPCServerSession) async throws -> XPCMessage
/// Wraps a session-unaware handler for use in a route table.
public static func route(
_ fn: @Sendable @escaping (XPCMessage) async throws -> XPCMessage
) -> RouteHandler {
{ message, _ in try await fn(message) }
}
private let routes: [String: RouteHandler]
// Access to `connection` is protected by a lock.
private nonisolated(unsafe) let connection: xpc_connection_t
private let lock = NSLock()
let log: Logging.Logger
public init(identifier: String, routes: [String: RouteHandler], log: Logging.Logger) {
let connection = xpc_connection_create_mach_service(
identifier,
nil,
UInt64(XPC_CONNECTION_MACH_SERVICE_LISTENER)
)
self.routes = routes
self.connection = connection
self.log = log
}
public init(connection: xpc_connection_t, routes: [String: RouteHandler], log: Logging.Logger) {
self.routes = routes
self.connection = connection
self.log = log
}
public func listen() async throws {
let connections = AsyncStream<xpc_connection_t> { cont in
lock.withLock {
xpc_connection_set_event_handler(self.connection) { object in
switch xpc_get_type(object) {
case XPC_TYPE_CONNECTION:
// `object` isn't used concurrently.
nonisolated(unsafe) let object = object
cont.yield(object)
case XPC_TYPE_ERROR:
if object.connectionError {
cont.finish()
}
default:
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
}
}
}
}
defer {
lock.withLock {
xpc_connection_cancel(self.connection)
}
}
lock.withLock {
xpc_connection_activate(self.connection)
}
try await withThrowingDiscardingTaskGroup { group in
for await conn in connections {
// `conn` isn't used concurrently.
nonisolated(unsafe) let conn = conn
let added = group.addTaskUnlessCancelled { @Sendable in
try await self.handleClientConnection(connection: conn)
xpc_connection_cancel(conn)
}
if !added {
break
}
}
group.cancelAll()
}
}
func handleClientConnection(connection: xpc_connection_t) async throws {
let replySent = Mutex(false)
let session = XPCServerSession()
let objects = AsyncStream<xpc_object_t> { cont in
xpc_connection_set_event_handler(connection) { object in
switch xpc_get_type(object) {
case XPC_TYPE_DICTIONARY:
// `object` isn't used concurrently.
nonisolated(unsafe) let object = object
cont.yield(object)
case XPC_TYPE_ERROR:
if object.connectionError {
cont.finish()
}
if !(replySent.withLock({ $0 }) && object.connectionClosed) {
// When a xpc connection is closed, the framework sends
// a final XPC_ERROR_CONNECTION_INVALID message.
// We can ignore this if we know we have already handled
// the request.
self.log.error(
"xpc client handler connection error",
metadata: [
"error": "\(object.errorDescription ?? "no description")"
])
}
default:
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
}
}
}
defer {
xpc_connection_cancel(connection)
}
xpc_connection_activate(connection)
try await withThrowingDiscardingTaskGroup { group in
// `connection` isn't used concurrently.
nonisolated(unsafe) let connection = connection
for await object in objects {
// `object` isn't used concurrently.
nonisolated(unsafe) let object = object
let added = group.addTaskUnlessCancelled { @Sendable in
try await self.handleMessage(connection: connection, object: object, session: session)
replySent.withLock { $0 = true }
}
if !added {
break
}
}
group.cancelAll()
}
await session.fireDisconnect()
}
func handleMessage(connection: xpc_connection_t, object: xpc_object_t, session: XPCServerSession) async throws {
// All requests are dictionary-valued.
guard xpc_get_type(object) == XPC_TYPE_DICTIONARY else {
log.error("invalid request - not a dictionary")
Self.replyWithError(
connection: connection,
object: object,
err: ContainerizationError(.invalidArgument, message: "invalid request")
)
return
}
// Ensure that the client has our EUID
var token = audit_token_t()
xpc_dictionary_get_audit_token(object, &token)
let serverEuid = geteuid()
let clientEuid = audit_token_to_euid(token)
guard clientEuid == serverEuid else {
log.error(
"unauthorized request - uid mismatch",
metadata: [
"server_euid": "\(serverEuid)",
"client_euid": "\(clientEuid)",
])
Self.replyWithError(
connection: connection,
object: object,
err: ContainerizationError(.invalidState, message: "unauthorized request")
)
return
}
guard let route = object.route else {
log.error("invalid request - empty route")
Self.replyWithError(
connection: connection,
object: object,
err: ContainerizationError(.invalidArgument, message: "invalid request")
)
return
}
if let handler = routes[route] {
do {
let message = XPCMessage(object: object)
let response = try await handler(message, session)
xpc_connection_send_message(connection, response.underlying)
} catch let error as ContainerizationError {
log.error(
"route handler threw an error",
metadata: [
"route": "\(route)",
"error": "\(error)",
])
Self.replyWithError(
connection: connection,
object: object,
err: error
)
} catch {
log.error(
"route handler threw an error",
metadata: [
"route": "\(route)",
"error": "\(error)",
])
let message = XPCMessage(object: object)
let reply = message.reply()
// Check if this is a VolumeError by looking at the error description
let errorMessage = error.localizedDescription
let errorTypeString = String(describing: type(of: error))
if errorTypeString.contains("VolumeError") || errorMessage.contains("Volume") {
let err = ContainerizationError(.invalidArgument, message: errorMessage)
reply.set(error: err)
} else {
let err = ContainerizationError(.unknown, message: String(describing: error))
reply.set(error: err)
}
xpc_connection_send_message(connection, reply.underlying)
}
}
}
private static func replyWithError(connection: xpc_connection_t, object: xpc_object_t, err: ContainerizationError) {
let message = XPCMessage(object: object)
let reply = message.reply()
reply.set(error: err)
xpc_connection_send_message(connection, reply.underlying)
}
}
extension xpc_object_t {
var route: String? {
let croute = xpc_dictionary_get_string(self, XPCMessage.routeKey)
guard let croute else {
return nil
}
return String(cString: croute)
}
var connectionError: Bool {
precondition(isError, "not an error")
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID) || xpc_equal(self, XPC_ERROR_CONNECTION_INTERRUPTED)
}
var connectionClosed: Bool {
precondition(isError, "not an error")
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID)
}
var isError: Bool {
xpc_get_type(self) == XPC_TYPE_ERROR
}
var errorDescription: String? {
precondition(isError, "not an error")
let cstring = xpc_dictionary_get_string(self, XPC_ERROR_KEY_DESCRIPTION)
guard let cstring else {
return nil
}
return String(cString: cstring)
}
}
#endif
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
/// Represents a single client connection on the server side.
///
/// The server creates one `XPCServerSession` per accepted client connection.
/// Handlers that need to associate state with a connection (e.g. resource
/// tracking for automatic cleanup) receive the session as a parameter and
/// may register disconnect handlers via `onDisconnect(_:)`.
public actor XPCServerSession {
private var disconnectHandlers: [@Sendable () async -> Void] = []
public init() {}
/// Register a handler to be called when the client connection closes.
public func onDisconnect(_ handler: @Sendable @escaping () async -> Void) {
disconnectHandlers.append(handler)
}
func fireDisconnect() async {
for handler in disconnectHandlers { await handler() }
}
}
extension XPCServerSession: Hashable {
public nonisolated static func == (lhs: XPCServerSession, rhs: XPCServerSession) -> Bool {
lhs === rhs
}
public nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
#endif