mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
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
218 lines
9.3 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|