From a2b98baa53fbf383fddf84528e23f5e3ba86ad15 Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 5 Sep 2026 22:17:27 +0100 Subject: [PATCH] fix(xpc): reject unknown peer error codes --- Package.swift | 6 ++ Sources/ContainerXPC/XPCMessage.swift | 24 ++++++- Tests/ContainerXPCTests/XPCMessageTests.swift | 71 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 Tests/ContainerXPCTests/XPCMessageTests.swift diff --git a/Package.swift b/Package.swift index b2f5ba69c..8772772d7 100644 --- a/Package.swift +++ b/Package.swift @@ -524,6 +524,12 @@ let package = Package( "CAuditToken", ] ), + .testTarget( + name: "ContainerXPCTests", + dependencies: [ + "ContainerXPC" + ] + ), .target( name: "ContainerOS", dependencies: [ diff --git a/Sources/ContainerXPC/XPCMessage.swift b/Sources/ContainerXPC/XPCMessage.swift index 612963286..798c590eb 100644 --- a/Sources/ContainerXPC/XPCMessage.swift +++ b/Sources/ContainerXPC/XPCMessage.swift @@ -51,6 +51,23 @@ public struct XPCMessage: Sendable { } extension XPCMessage { + private static let errorCodesByDescription: [String: ContainerizationError.Code] = { + let codes: [ContainerizationError.Code] = [ + .unknown, + .invalidArgument, + .internalError, + .exists, + .notFound, + .cancelled, + .invalidState, + .empty, + .timeout, + .unsupported, + .interrupted, + ] + return Dictionary(uniqueKeysWithValues: codes.map { ($0.description, $0) }) + }() + public static func == (lhs: XPCMessage, rhs: xpc_object_t) -> Bool { xpc_equal(lhs.underlying, rhs) } @@ -78,14 +95,17 @@ extension XPCMessage { public func error() throws { let data = data(key: Self.errorKey) if let data { - guard let item = try? JSONDecoder().decode(ContainerXPCError.self, from: data) else { + guard + let item = try? JSONDecoder().decode(ContainerXPCError.self, from: data), + let code = Self.errorCodesByDescription[item.code] + else { throw ContainerizationError( .internalError, message: "received a malformed error payload from the XPC peer" ) } - throw ContainerizationError(item.code, message: item.message) + throw ContainerizationError(code, message: item.message) } } diff --git a/Tests/ContainerXPCTests/XPCMessageTests.swift b/Tests/ContainerXPCTests/XPCMessageTests.swift new file mode 100644 index 000000000..7fa7a5d22 --- /dev/null +++ b/Tests/ContainerXPCTests/XPCMessageTests.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// 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 ContainerizationError +import Foundation +import Testing + +@testable import ContainerXPC + +@Suite +struct XPCMessageTests { + @Test + func errorRejectsUnknownPeerCodeWithoutTrapping() throws { + let message = XPCMessage(route: "unknown-error-code") + message.set( + key: XPCMessage.errorKey, + value: Data(#"{"code":"newerPeerCode","message":"from a newer peer"}"#.utf8) + ) + + let error = try #require( + #expect(throws: ContainerizationError.self) { + try message.error() + } + ) + #expect(error.code == .internalError) + #expect(error.message == "received a malformed error payload from the XPC peer") + } + + @Test( + arguments: [ + ContainerizationError.Code.unknown, + .invalidArgument, + .internalError, + .exists, + .notFound, + .cancelled, + .invalidState, + .empty, + .timeout, + .unsupported, + .interrupted, + ] + ) + func errorPreservesKnownPeerCodeAndMessage(code: ContainerizationError.Code) throws { + let message = XPCMessage(route: "known-error-code") + message.set(error: ContainerizationError(code, message: "message")) + + let error = try #require( + #expect(throws: ContainerizationError.self) { + try message.error() + } + ) + #expect(error.code == code) + #expect(error.message == "message") + } +} +#endif