mirror of
https://github.com/apple/container.git
synced 2026-10-14 01:59:18 +00:00
Fix container system status false negative under a Seatbelt sandbox. (#2316)
- `launchctl list` does not work as expected in the sandbox. - Probe with `launchctl print <domain>/<label>` instead, which launchd does allow a sandboxed process, and key off exit codes: 0 means registered, 113 (ENOSERVICE) means not registered, and anything else is an error rather than being mistranslated into a negative. - The probe is advisory in `system status`, so an indeterminate answer falls through to the XPC health check, whose success is authoritative. Signed-off-by: Renlord Yang <renlord@apple.com>
This commit is contained in:
1 parent
8ba7e2a65a
commit
175e493a6f
3 files changed
+95
-3
No files matched your search
@@ -42,7 +42,8 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let isRegistered = try ServiceManager.isRegistered(fullServiceLabel: "\(prefix)apiserver")
|
||||
let domain = try ServiceManager.getDomainString()
|
||||
let isRegistered = (try? ServiceManager.isRegistered(fullServiceLabel: "\(domain)/\(prefix)apiserver")) ?? true
|
||||
if !isRegistered {
|
||||
try Output.render(payload: StatusPayload(status: "unregistered"), format: format) {
|
||||
"apiserver is not running and not registered with launchd"
|
||||
|
||||
@@ -18,6 +18,10 @@ import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
public struct ServiceManager {
|
||||
enum LaunchctlStatus {
|
||||
static let noSuchService: Int32 = 113
|
||||
}
|
||||
|
||||
private static func runLaunchctlCommand(args: [String]) throws -> Int32 {
|
||||
let launchctl = Foundation.Process()
|
||||
launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
||||
@@ -94,8 +98,40 @@ public struct ServiceManager {
|
||||
|
||||
/// Check if a service has been registered or not.
|
||||
public static func isRegistered(fullServiceLabel label: String) throws -> Bool {
|
||||
let exitStatus = try runLaunchctlCommand(args: ["list", label])
|
||||
return exitStatus == 0
|
||||
let result = try runLaunchctlPrint(target: label)
|
||||
return try Self.interpretPrintStatus(result.status, target: label, standardError: result.standardError)
|
||||
}
|
||||
|
||||
private static func runLaunchctlPrint(target: String) throws -> (status: Int32, standardError: String) {
|
||||
let launchctl = Foundation.Process()
|
||||
launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
||||
launchctl.arguments = ["print", target]
|
||||
|
||||
let stderrPipe = Pipe()
|
||||
launchctl.standardOutput = FileHandle.nullDevice
|
||||
launchctl.standardError = stderrPipe
|
||||
|
||||
try launchctl.run()
|
||||
let errorData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
launchctl.waitUntilExit()
|
||||
|
||||
return (launchctl.terminationStatus, String(decoding: errorData, as: UTF8.self))
|
||||
}
|
||||
|
||||
static func interpretPrintStatus(_ status: Int32, target: String = "", standardError: String = "") throws -> Bool {
|
||||
switch status {
|
||||
case 0:
|
||||
return true
|
||||
case LaunchctlStatus.noSuchService:
|
||||
return false
|
||||
default:
|
||||
var message = "command `launchctl print \(target)` failed with status \(status)"
|
||||
let details = standardError.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !details.isEmpty {
|
||||
message += ", message: \(details)"
|
||||
}
|
||||
throw ContainerizationError(.internalError, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
private static func getLaunchdSessionType() throws -> String {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerPlugin
|
||||
|
||||
struct ServiceManagerTests {
|
||||
@Test
|
||||
func testInterpretPrintStatusSuccessMeansRegistered() throws {
|
||||
#expect(try ServiceManager.interpretPrintStatus(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInterpretPrintStatusNoSuchServiceMeansUnregistered() throws {
|
||||
#expect(try !ServiceManager.interpretPrintStatus(ServiceManager.LaunchctlStatus.noSuchService))
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInterpretPrintStatusUnexpectedStatusThrows() throws {
|
||||
let statuses: [Int32] = [1, 3, 64, 112]
|
||||
for status in statuses {
|
||||
#expect {
|
||||
_ = try ServiceManager.interpretPrintStatus(
|
||||
status,
|
||||
target: "gui/501/com.apple.container.apiserver",
|
||||
standardError: "marker"
|
||||
)
|
||||
} throws: { error in
|
||||
"\(error)".contains("status \(status), message: marker")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testIsRegisteredUnknownLabel() throws {
|
||||
let domain = try ServiceManager.getDomainString()
|
||||
let label = "\(domain)/com.apple.container.bogus-\(UUID().uuidString)"
|
||||
#expect(try !ServiceManager.isRegistered(fullServiceLabel: label))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user