mirror of
https://github.com/apple/container.git
synced 2026-07-13 04:57:08 +00:00
d6f052d206
## Motivation and Context
Now that we're in 2026, we need to update the license headers on all the
files. Unfortunately, Hawkeye doesn't have an attribute for the current
year to help us avoid this in the future. Instead, I had to work around
this by doing the following:
1. Update licenserc.toml with:
```
[properties]
... (other properties)
currentYear = "2026"
```
2. Update scripts/license-header.txt with
```
Copyright ©{{ " " }}{%- set created = attrs.git_file_created_year or
attrs.disk_file_created_year -%}{%- set modified = props["currentYear"]
-%}{%- if created != modified -%} {{created}}-{{modified}}{%- else
-%}{{created}}{%- endif -%}{{ " " }}{{ props["copyrightOwner"] }}.
```
Then I removed these two changes before committing. After this PR is
merged, all files will have recently had git updates, so the existing
code for setting the modified year should work as intended.
Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
266 lines
9.6 KiB
Swift
266 lines
9.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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) async throws -> XPCMessage
|
|
|
|
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 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 \(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)
|
|
replySent.withLock { $0 = true }
|
|
}
|
|
if !added {
|
|
break
|
|
}
|
|
}
|
|
group.cancelAll()
|
|
}
|
|
}
|
|
|
|
func handleMessage(connection: xpc_connection_t, object: xpc_object_t) 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)
|
|
xpc_connection_send_message(connection, response.underlying)
|
|
} catch let error as ContainerizationError {
|
|
log.error("handler for \(route) threw error \(error)")
|
|
Self.replyWithError(
|
|
connection: connection,
|
|
object: object,
|
|
err: error
|
|
)
|
|
} catch {
|
|
log.error("handler for \(route) threw 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
|