Files
UTM/Configuration/UTMAppleConfigurationBoot.swift
osy 855bc0d2df config: add Secure Boot option for Apple UEFI guests
macOS 27 lets the host manage Secure Boot state in the EFI variable
store. Add a persisted flag and a toggle in the Boot settings so generic
UEFI guests on the Apple backend can opt in. The state is reconciled when
the configuration is saved, using the default platform key and the
default Microsoft signatures so that mainstream Linux distributions boot
unmodified. The store is only written when its state differs from the
configuration, and a virtual machine with the option off never fails to
save because of it. The option defaults to off because guests without a
Microsoft-signed bootloader would otherwise stop booting.

Assisted-by: Claude:claude-fable-5-1
2026-09-19 23:18:45 -05:00

218 lines
9.3 KiB
Swift

//
// Copyright © 2022 osy. All rights reserved.
//
// 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
//
// http://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 Virtualization
@available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
struct UTMAppleConfigurationBoot: Codable {
enum OperatingSystem: String, CaseIterable, QEMUConstant {
case none = "None"
case linux = "Linux"
case macOS = "macOS"
var prettyValue: String {
switch self {
case .none: return NSLocalizedString("None", comment: "UTMAppleConfigurationBoot")
case .linux: return NSLocalizedString("Linux", comment: "UTMAppleConfigurationBoot")
case .macOS: return NSLocalizedString("macOS", comment: "UTMAppleConfigurationBoot")
}
}
}
var operatingSystem: OperatingSystem
var linuxKernelURL: URL?
var linuxCommandLine: String?
var linuxInitialRamdiskURL: URL?
var efiVariableStorageURL: URL?
var vmSavedStateURL: URL?
var hasUefiBoot: Bool = false
var hasSecureBoot: Bool = false
/// IPSW for installing macOS. Not saved.
var macRecoveryIpswURL: URL?
private enum CodingKeys: String, CodingKey {
case operatingSystem = "OperatingSystem"
case linuxKernelPath = "LinuxKernelPath"
case linuxCommandLine = "LinuxCommandLine"
case linuxInitialRamdiskPath = "LinuxInitialRamdiskPath"
case efiVariableStoragePath = "EfiVariableStoragePath"
case hasUefiBoot = "UEFIBoot"
case hasSecureBoot = "SecureBoot"
}
init(from decoder: Decoder) throws {
guard let dataURL = decoder.userInfo[.dataURL] as? URL else {
throw UTMConfigurationError.invalidDataURL
}
let container = try decoder.container(keyedBy: CodingKeys.self)
operatingSystem = try container.decode(OperatingSystem.self, forKey: .operatingSystem)
hasUefiBoot = try container.decodeIfPresent(Bool.self, forKey: .hasUefiBoot) ?? false
hasSecureBoot = try container.decodeIfPresent(Bool.self, forKey: .hasSecureBoot) ?? false
if let linuxKernelPath = try container.decodeIfPresent(String.self, forKey: .linuxKernelPath) {
linuxKernelURL = dataURL.appendingPathComponent(linuxKernelPath)
}
linuxCommandLine = try container.decodeIfPresent(String.self, forKey: .linuxCommandLine)
if let linuxInitialRamdiskPath = try container.decodeIfPresent(String.self, forKey: .linuxInitialRamdiskPath) {
linuxInitialRamdiskURL = dataURL.appendingPathComponent(linuxInitialRamdiskPath)
}
if let efiVariableStoragePath = try container.decodeIfPresent(String.self, forKey: .efiVariableStoragePath) {
efiVariableStorageURL = dataURL.appendingPathComponent(efiVariableStoragePath)
}
vmSavedStateURL = dataURL.appendingPathComponent(QEMUPackageFileName.vmState.rawValue)
}
init(for operatingSystem: OperatingSystem, linuxKernelURL: URL? = nil) throws {
self.operatingSystem = operatingSystem
self.linuxKernelURL = linuxKernelURL
if operatingSystem == .linux && linuxKernelURL == nil {
self.hasUefiBoot = true
}
}
init(from linux: VZLinuxBootLoader) {
self.operatingSystem = .linux
self.linuxKernelURL = linux.kernelURL
self.linuxCommandLine = linux.commandLine
self.linuxInitialRamdiskURL = linux.initialRamdiskURL
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(operatingSystem, forKey: .operatingSystem)
try container.encode(hasUefiBoot, forKey: .hasUefiBoot)
try container.encode(hasSecureBoot, forKey: .hasSecureBoot)
if operatingSystem == .linux {
try container.encodeIfPresent(linuxKernelURL?.lastPathComponent, forKey: .linuxKernelPath)
try container.encodeIfPresent(linuxCommandLine, forKey: .linuxCommandLine)
try container.encodeIfPresent(linuxInitialRamdiskURL?.lastPathComponent, forKey: .linuxInitialRamdiskPath)
try container.encodeIfPresent(efiVariableStorageURL?.lastPathComponent, forKey: .efiVariableStoragePath)
}
}
func vzBootloader() -> VZBootLoader? {
switch operatingSystem {
case .none:
return nil
case .linux:
if #available(macOS 13, *), let efiVariableStorageURL = efiVariableStorageURL, hasUefiBoot {
let efi = VZEFIBootLoader()
efi.variableStore = VZEFIVariableStore(url: efiVariableStorageURL)
return efi
}
guard let linuxKernelURL = linuxKernelURL else {
return nil
}
let linux = VZLinuxBootLoader(kernelURL: linuxKernelURL)
linux.initialRamdiskURL = linuxInitialRamdiskURL
if let linuxCommandLine = linuxCommandLine {
linux.commandLine = linuxCommandLine
}
return linux
case .macOS:
#if arch(arm64)
return VZMacOSBootLoader()
#else
return nil
#endif
}
}
}
// MARK: - Conversion of old config format
@available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
extension UTMAppleConfigurationBoot {
init(migrating oldBoot: Bootloader) {
switch oldBoot.operatingSystem {
case .macOS: operatingSystem = .macOS
case .Linux: operatingSystem = .linux
}
linuxKernelURL = oldBoot.linuxKernelURL
linuxCommandLine = oldBoot.linuxCommandLine
linuxInitialRamdiskURL = oldBoot.linuxInitialRamdiskURL
}
}
// MARK: - Saving data
@available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
extension UTMAppleConfigurationBoot {
@MainActor mutating func saveData(to dataURL: URL) async throws -> [URL] {
var urls = [URL]()
if operatingSystem == .linux && !hasUefiBoot {
guard let linuxKernelURL = linuxKernelURL else {
throw UTMAppleConfigurationError.kernelNotSpecified
}
let kernelUrl = try await UTMAppleConfiguration.copyItemIfChanged(from: linuxKernelURL, to: dataURL)
self.linuxKernelURL = kernelUrl
urls.append(kernelUrl)
if let linuxInitialRamdiskURL = linuxInitialRamdiskURL {
let ramdiskUrl = try await UTMAppleConfiguration.copyItemIfChanged(from: linuxInitialRamdiskURL, to: dataURL)
self.linuxInitialRamdiskURL = ramdiskUrl
urls.append(ramdiskUrl)
}
self.efiVariableStorageURL = nil
}
if hasUefiBoot {
guard #available(macOS 13, *) else {
throw UTMAppleConfigurationError.platformUnsupported
}
let fileManager = FileManager.default
let efiVariableStorageURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
let variableStore: VZEFIVariableStore
if fileManager.fileExists(atPath: efiVariableStorageURL.path) {
variableStore = VZEFIVariableStore(url: efiVariableStorageURL)
} else {
variableStore = try VZEFIVariableStore(creatingVariableStoreAt: efiVariableStorageURL)
}
if #available(macOS 27, *) {
try updateSecureBoot(in: variableStore)
}
self.linuxKernelURL = nil
self.linuxInitialRamdiskURL = nil
self.linuxCommandLine = nil
self.efiVariableStorageURL = efiVariableStorageURL
urls.append(efiVariableStorageURL)
}
let vmSavedStateURL = dataURL.appendingPathComponent(QEMUPackageFileName.vmState.rawValue)
self.vmSavedStateURL = vmSavedStateURL
urls.append(vmSavedStateURL)
return urls
}
/// Secure Boot state lives in the variable store, so only write to it when it differs from the configuration.
@available(macOS 27, *)
private func updateSecureBoot(in variableStore: VZEFIVariableStore) throws {
do {
if hasSecureBoot {
guard try !variableStore.isSecureBootEnabled else {
return
}
try variableStore.enrollDefaultSecureBootSignatures()
try variableStore.enableSecureBootUsingDefaultPlatformKey()
} else if (try? variableStore.isSecureBootEnabled) == true {
try variableStore.disableSecureBoot()
}
} catch let error as VZError where error.code == .efiSecureBootEnrollmentFailed {
throw UTMAppleConfigurationError.secureBootEnrollmentFailed
} catch let error as VZError where error.code == .efiVariableInaccessible {
throw UTMAppleConfigurationError.efiVariablesInaccessible
}
}
}