Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions macos/ForgePlatformInstaller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ startup. It asks for explicit confirmation without `--yes`, rechecks installer
currency immediately before `SMAppService` registration, and reports native
`ENABLED` or `REQUIRES_APPROVAL` separately. Registration never implies that
the product-operation backend or reboot persistence is ready.
An `ENABLED` system job is accepted only after a fixed-label native readback
confirms the registered parent bundle, Team, helper program and exact current
installer version. An older registered helper fails closed; registration does
not silently replace it. A separate reviewed transition is required.
On a Mac where the service has never been seen, native status can initially be
`NOT_FOUND`; the fixed bundled service is registered once and its status read
again. A registration error accompanied by native `REQUIRES_APPROVAL` remains
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import ForgePlatformInstallerCore
/// Registers the bundled helper only after the released CLI has passed trusted
/// startup, an explicit operator decision and a fresh installer-currency read.
enum InstallerCLIHelperRegistration {
typealias Registrar = @Sendable () async -> ManagedInstallerPrivilegedHelperRegistrationResult
typealias Registrar = @Sendable (InstallerVersion) async -> ManagedInstallerPrivilegedHelperRegistrationResult

static let liveRegistrar: Registrar = {
static let liveRegistrar: Registrar = { expectedVersion in
let service = MacOSManagedInstallerPrivilegedHelperServiceController()
return await ManagedInstallerPrivilegedHelperRegistrationCoordinator(
service: service
).ensureRegistered()
).ensureRegistered(expectedVersion: expectedVersion)
}

static func run(
Expand All @@ -35,7 +35,8 @@ enum InstallerCLIHelperRegistration {

switch await startup.start(currentVersion: currentVersion) {
case .ready(let freshRelease, _):
guard freshRelease == currentRelease else {
guard freshRelease == currentRelease,
freshRelease.version == currentVersion else {
return blocked("De geverifieerde installerrelease veranderde vóór helperregistratie.")
}
case .updateRequired(let release), .relaunching(let release):
Expand All @@ -49,7 +50,7 @@ enum InstallerCLIHelperRegistration {
return blocked(reason)
}

switch await register() {
switch await register(currentVersion) {
case .ready(let receipt):
return InstallerCLIResult(
exitCode: .success,
Expand All @@ -70,6 +71,7 @@ enum InstallerCLIHelperRegistration {
case .registrationFailed: reason = "registration-failed"
case .serviceUnavailable: reason = "service-unavailable"
case .statusDrift: reason = "status-drift"
case .registeredParentMismatch: reason = "registered-parent-mismatch"
}
return InstallerCLIResult(
exitCode: .executionFailed,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Darwin
import Foundation
@preconcurrency import ServiceManagement

Expand All @@ -24,6 +25,7 @@ public enum ManagedInstallerPrivilegedHelperRegistrationFailure:
case registrationFailed
case serviceUnavailable
case statusDrift
case registeredParentMismatch
}

public struct ManagedInstallerPrivilegedHelperRegistrationReceipt:
Expand Down Expand Up @@ -55,6 +57,7 @@ public enum ManagedInstallerPrivilegedHelperRegistrationResult:

public protocol ManagedInstallerPrivilegedHelperServiceControlling: Sendable {
func readStatus() -> ManagedInstallerPrivilegedHelperStatus
func readRegisteredParentVersion() -> InstallerVersion?
func register() throws
}

Expand All @@ -65,21 +68,25 @@ public final class MacOSManagedInstallerPrivilegedHelperServiceController:
ManagedInstallerPrivilegedHelperServiceControlling, @unchecked Sendable {
private let statusReader: @Sendable () -> SMAppService.Status
private let registrar: @Sendable () throws -> Void
private let parentVersionReader: @Sendable () -> InstallerVersion?

public init() {
let service = SMAppService.daemon(
plistName: ManagedInstallerPrivilegedHelperContract.plistName
)
statusReader = { service.status }
registrar = { try service.register() }
parentVersionReader = { RegisteredInstallerHelperParentReader().readVersion() }
}

init(
statusReader: @escaping @Sendable () -> SMAppService.Status,
registrar: @escaping @Sendable () throws -> Void
registrar: @escaping @Sendable () throws -> Void,
parentVersionReader: @escaping @Sendable () -> InstallerVersion? = { nil }
) {
self.statusReader = statusReader
self.registrar = registrar
self.parentVersionReader = parentVersionReader
}

public func readStatus() -> ManagedInstallerPrivilegedHelperStatus {
Expand All @@ -95,6 +102,68 @@ public final class MacOSManagedInstallerPrivilegedHelperServiceController:
public func register() throws {
try registrar()
}

public func readRegisteredParentVersion() -> InstallerVersion? {
parentVersionReader()
}
}

/// Inspects only the fixed system job. The parent identity is an independent
/// ServiceManagement readback, never a version supplied by the CLI caller.
struct RegisteredInstallerHelperParentReader: Sendable {
func readVersion() -> InstallerVersion? {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/launchctl")
process.arguments = ["print", "system/" + ManagedInstallerPrivilegedHelperContract.label]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
let collector = BoundedProcessOutputCollector(maximumBytes: 64 * 1024)
let finished = DispatchSemaphore(value: 0)
process.terminationHandler = { _ in finished.signal() }
do { try process.run() } catch { return nil }
let drained = DispatchGroup()
drained.enter()
DispatchQueue.global(qos: .userInitiated).async {
defer { drained.leave() }
collector.consume(pipe.fileHandleForReading)
}
guard finished.wait(timeout: .now() + .seconds(5)) == .success else {
process.terminate()
if finished.wait(timeout: .now() + .seconds(2)) != .success {
_ = Darwin.kill(process.processIdentifier, SIGKILL)
_ = finished.wait(timeout: .now() + .seconds(2))
}
return nil
}
guard drained.wait(timeout: .now() + .seconds(2)) == .success,
process.terminationReason == .exit,
process.terminationStatus == 0,
let data = collector.collectedData(),
let output = String(data: data, encoding: .utf8) else { return nil }
return Self.parse(output)
}

static func parse(_ output: String) -> InstallerVersion? {
let lines = output.split(separator: "\n").map { $0.trimmingCharacters(in: .whitespaces) }
func uniqueValue(_ key: String) -> String? {
let matches = lines.compactMap { line -> String? in
let prefix = key + " = "
return line.hasPrefix(prefix) ? String(line.dropFirst(prefix.count)) : nil
}
return matches.count == 1 ? matches[0] : nil
}
guard lines.first == "system/\(ManagedInstallerPrivilegedHelperContract.label) = {",
uniqueValue("managed_by") == "com.apple.xpc.ServiceManagement",
lines.filter({ $0 == "\"team-identifier\" => \"ZEML4LPXH4\"" }).count == 1,
lines.filter({ $0 == "\"signing-identifier\" => \"\(ManagedInstallerPrivilegedHelperContract.label)\"" }).count == 1,
uniqueValue("parent bundle identifier") ==
ManagedInstallerHelperSignedParentBundleLocator.bundleIdentifier,
uniqueValue("program identifier") ==
ManagedInstallerPrivilegedHelperContract.bundleProgram + " (mode: 2)",
let raw = uniqueValue("parent bundle version") else { return nil }
return try? InstallerVersion(raw)
}
}

/// Registers the bundled system daemon once and immediately reads its state
Expand All @@ -108,10 +177,12 @@ public actor ManagedInstallerPrivilegedHelperRegistrationCoordinator {
self.service = service
}

public func ensureRegistered() -> ManagedInstallerPrivilegedHelperRegistrationResult {
public func ensureRegistered(
expectedVersion: InstallerVersion
) -> ManagedInstallerPrivilegedHelperRegistrationResult {
switch service.readStatus() {
case .enabled:
return receipt(for: .enabled)
return verifiedReady(expectedVersion: expectedVersion)
case .requiresApproval:
return receipt(for: .requiresApproval)
case .notRegistered, .notFound:
Expand All @@ -129,7 +200,7 @@ public actor ManagedInstallerPrivilegedHelperRegistrationCoordinator {
}
switch service.readStatus() {
case .enabled:
return receipt(for: .enabled)
return verifiedReady(expectedVersion: expectedVersion)
case .requiresApproval:
return receipt(for: .requiresApproval)
case .notRegistered:
Expand All @@ -140,6 +211,17 @@ public actor ManagedInstallerPrivilegedHelperRegistrationCoordinator {
}
}

private func verifiedReady(
expectedVersion: InstallerVersion
) -> ManagedInstallerPrivilegedHelperRegistrationResult {
guard service.readRegisteredParentVersion() == expectedVersion,
service.readStatus() == .enabled,
service.readRegisteredParentVersion() == expectedVersion else {
return .failed(.registeredParentMismatch)
}
return receipt(for: .enabled)
}

private func receipt(
for status: ManagedInstallerPrivilegedHelperStatus
) -> ManagedInstallerPrivilegedHelperRegistrationResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
["helper", "register", "--non-interactive"],
startup: deniedStartup,
version: "1.2.3",
registration: { await registrar.invoke() }
registration: { _ in await registrar.invoke() }
)
XCTAssertEqual(denied.code, InstallerCLIExitCode.confirmationRequired.rawValue)
let deniedStarts = await deniedStartup.startCalls()
Expand All @@ -559,7 +559,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
["helper", "register", "--yes", "--non-interactive", "--json"],
startup: readyStartup,
version: "1.2.3",
registration: { await registrar.invoke() }
registration: { _ in await registrar.invoke() }
)
XCTAssertEqual(enabled.code, InstallerCLIExitCode.success.rawValue)
XCTAssertTrue(enabled.stdout.joined().contains("helper-enabled"))
Expand All @@ -582,7 +582,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
currentRelease: current, coordinator: CLIReadyCoordinator()
)),
version: "1.2.3",
registration: { await approval.invoke() }
registration: { _ in await approval.invoke() }
)
XCTAssertEqual(approved.code, InstallerCLIExitCode.interactionRequired.rawValue)
XCTAssertTrue(approved.stderr.joined().contains("REQUIRES_APPROVAL"))
Expand All @@ -591,14 +591,15 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
ManagedInstallerPrivilegedHelperRegistrationFailure.registrationFailed,
.serviceUnavailable,
.statusDrift,
.registeredParentMismatch,
] {
let failed = await run(
["helper", "register", "--yes"],
startup: CLIStartupSpy(outcome: .ready(
currentRelease: current, coordinator: CLIReadyCoordinator()
)),
version: "1.2.3",
registration: { .failed(failure) }
registration: { _ in .failed(failure) }
)
XCTAssertEqual(failed.code, InstallerCLIExitCode.executionFailed.rawValue)
XCTAssertTrue(failed.stderr.joined().contains("helper-registration-failed"))
Expand All @@ -612,7 +613,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
["helper", "register", "--yes"],
startup: recheck,
version: "1.2.3",
registration: { await approval.invoke() }
registration: { _ in await approval.invoke() }
)
XCTAssertEqual(denied.code, InstallerCLIExitCode.installerUpdateRequired.rawValue)
let recheckStarts = await recheck.startCalls()
Expand All @@ -627,7 +628,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
recheckOutcome: .ready(currentRelease: next, coordinator: CLIReadyCoordinator())
),
version: "1.2.3",
registration: { await approval.invoke() }
registration: { _ in await approval.invoke() }
)
XCTAssertEqual(drift.code, InstallerCLIExitCode.blocked.rawValue)
let driftCalls = await approval.calls()
Expand All @@ -639,7 +640,7 @@ final class ForgePlatformInstallerCLIApplicationTests: XCTestCase {
startup: CLIStartupSpy,
version: String?,
confirmation: Bool = true,
registration: @escaping InstallerCLIHelperRegistration.Registrar = {
registration: @escaping InstallerCLIHelperRegistration.Registrar = { _ in
.failed(.serviceUnavailable)
}
) async -> (code: Int32, stdout: [String], stderr: [String]) {
Expand Down
Loading
Loading