Adds client uid validation to XPC server. (#896)

- When a user performs an `su` the effective UID changes but the bootstrap
  mach port does not, so that if container is running as `alice` from a
  GUI login session, it's possible to `su bob` and continue running
  container. While this doesn't pose a significant security risk as it's
  necessary for Alice to know Bob's password and manually enter it with
  `su`, this change closes the loophole by validating that client UID from
  the caller's audit token matches that of the API server.
This commit is contained in:
J Logan
2025-11-19 00:41:56 -03:00
committed by GitHub
parent 266b135da2
commit b2f5f3ff2f
3 changed files with 87 additions and 9 deletions
+9
View File
@@ -303,6 +303,7 @@ let package = Package(
dependencies: [
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
"CAuditToken",
]
),
.target(
@@ -373,7 +374,15 @@ let package = Package(
.define("GIT_COMMIT", to: "\"\(gitCommit)\""),
.define("RELEASE_VERSION", to: "\"\(releaseVersion)\""),
.define("BUILDER_SHIM_VERSION", to: "\"\(builderShimVersion)\""),
],
linkerSettings: [
.linkedLibrary("bsm")
]
),
.target(
name: "CAuditToken",
dependencies: [],
publicHeadersPath: "include"
),
]
)
+20
View File
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 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.
//===----------------------------------------------------------------------===//
#include <xpc/xpc.h>
#include <bsm/libbsm.h>
void xpc_dictionary_get_audit_token(xpc_object_t xdict, audit_token_t *token);
+58 -9
View File
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
#if os(macOS)
import CAuditToken
import ContainerizationError
import Foundation
import Logging
@@ -25,7 +26,7 @@ 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
// Access to `connection` is protected by a lock.
private nonisolated(unsafe) let connection: xpc_connection_t
private let lock = NSLock()
@@ -112,8 +113,10 @@ public struct XPCServer: Sendable {
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.
// 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:
@@ -145,24 +148,63 @@ public struct XPCServer: Sendable {
}
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("empty route")
log.error("invalid request - empty route")
Self.replyWithError(
connection: connection,
object: object,
err: ContainerizationError(.invalidArgument, message: "invalid request")
)
return
}
if let handler = routes[route] {
let message = XPCMessage(object: object)
do {
let message = XPCMessage(object: object)
let response = try await handler(message)
xpc_connection_send_message(connection, response.underlying)
} catch let error as ContainerizationError {
let reply = message.reply()
log.error("handler for \(route) threw error \(error)")
reply.set(error: error)
xpc_connection_send_message(connection, reply.underlying)
Self.replyWithError(
connection: connection,
object: object,
err: error
)
} catch {
let reply = message.reply()
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
@@ -178,6 +220,13 @@ public struct XPCServer: Sendable {
}
}
}
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 {