From 483e38947f1e616593dfad1d4f2f4aa83a88c770 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:27:24 -0700 Subject: [PATCH] Validate container system stop --prefix to prevent accidental logout The --prefix option was interpolated unvalidated into launchd label and domain-target strings passed to launchctl bootout. A path value such as /usr/local/container produced a malformed bootout target, which can tear down the entire GUI login session and log the user out. Add a validate() guard that rejects any prefix that is empty or contains characters outside [A-Za-z0-9._-] (including '/'), with a clear error. All valid usage, including the default com.apple.container., is unchanged. Fixes #1672 --- .../ContainerCommands/System/SystemStop.swift | 15 +++++ .../SystemStopValidationTests.swift | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 Tests/ContainerCommandsTests/SystemStopValidationTests.swift diff --git a/Sources/ContainerCommands/System/SystemStop.swift b/Sources/ContainerCommands/System/SystemStop.swift index 164dd90fd..55338f81d 100644 --- a/Sources/ContainerCommands/System/SystemStop.swift +++ b/Sources/ContainerCommands/System/SystemStop.swift @@ -41,6 +41,21 @@ extension Application { public init() {} + public mutating func validate() throws { + guard !prefix.isEmpty, prefix.unicodeScalars.allSatisfy(Self.isValidLaunchdLabelPrefixScalar) else { + throw ValidationError("invalid --prefix \"\(prefix)\": must be a launchd label prefix (letters, digits, '.', '-', '_'), e.g. com.apple.container.") + } + } + + private static func isValidLaunchdLabelPrefixScalar(_ scalar: Unicode.Scalar) -> Bool { + switch scalar.value { + case 48...57, 65...90, 97...122, 45, 46, 95: + return true + default: + return false + } + } + public func run() async throws { let log = Logger( label: "com.apple.container.cli", diff --git a/Tests/ContainerCommandsTests/SystemStopValidationTests.swift b/Tests/ContainerCommandsTests/SystemStopValidationTests.swift new file mode 100644 index 000000000..22d498561 --- /dev/null +++ b/Tests/ContainerCommandsTests/SystemStopValidationTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import Testing + +@testable import ContainerCommands + +struct SystemStopValidationTests { + @Test + func rejectsPathPrefix() { + #expect { + try Self.parseAndValidate(["--prefix", "/usr/local/container"]) + } throws: { error in + String(describing: error).contains("invalid --prefix \"/usr/local/container\"") + } + } + + @Test + func rejectsInvalidCharacters() { + #expect { + try Self.parseAndValidate(["--prefix", "foo bar"]) + } throws: { error in + String(describing: error).contains("invalid --prefix \"foo bar\"") + } + } + + @Test + func acceptsDefaultPrefix() throws { + try Self.parseAndValidate([]) + } + + @Test + func acceptsCustomReverseDNSPrefix() throws { + try Self.parseAndValidate(["--prefix", "com.example.svc."]) + } + + private static func parseAndValidate(_ args: [String]) throws { + var command = try Application.SystemStop.parse(args) + try command.validate() + } +}