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
This commit is contained in:
osy
2026-09-19 23:18:45 -05:00
parent 9d00a54833
commit 855bc0d2df
3 changed files with 46 additions and 2 deletions
@@ -100,6 +100,8 @@ enum UTMAppleConfigurationError: Error {
case hardwareModelInvalid
case rosettaNotSupported
case featureNotSupported
case secureBootEnrollmentFailed
case efiVariablesInaccessible
}
extension UTMAppleConfigurationError: LocalizedError {
@@ -117,6 +119,10 @@ extension UTMAppleConfigurationError: LocalizedError {
return NSLocalizedString("Rosetta is not supported on the current host machine.", comment: "UTMAppleConfiguration")
case .featureNotSupported:
return NSLocalizedString("The host operating system needs to be updated to support one or more features requested by the guest.", comment: "UTMAppleConfiguration")
case .secureBootEnrollmentFailed:
return NSLocalizedString("Failed to set up Secure Boot for this virtual machine.", comment: "UTMAppleConfiguration")
case .efiVariablesInaccessible:
return NSLocalizedString("The UEFI variables of this virtual machine could not be accessed.", comment: "UTMAppleConfiguration")
}
}
}
+32 -2
View File
@@ -40,6 +40,7 @@ struct UTMAppleConfigurationBoot: Codable {
var efiVariableStorageURL: URL?
var vmSavedStateURL: URL?
var hasUefiBoot: Bool = false
var hasSecureBoot: Bool = false
/// IPSW for installing macOS. Not saved.
var macRecoveryIpswURL: URL?
@@ -51,6 +52,7 @@ struct UTMAppleConfigurationBoot: Codable {
case linuxInitialRamdiskPath = "LinuxInitialRamdiskPath"
case efiVariableStoragePath = "EfiVariableStoragePath"
case hasUefiBoot = "UEFIBoot"
case hasSecureBoot = "SecureBoot"
}
init(from decoder: Decoder) throws {
@@ -60,6 +62,7 @@ struct UTMAppleConfigurationBoot: Codable {
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)
}
@@ -92,6 +95,7 @@ struct UTMAppleConfigurationBoot: Codable {
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)
@@ -170,8 +174,14 @@ extension UTMAppleConfigurationBoot {
}
let fileManager = FileManager.default
let efiVariableStorageURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
if !fileManager.fileExists(atPath: efiVariableStorageURL.path) {
_ = try VZEFIVariableStore(creatingVariableStoreAt: efiVariableStorageURL)
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
@@ -184,4 +194,24 @@ extension UTMAppleConfigurationBoot {
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
}
}
}
@@ -76,6 +76,9 @@ struct VMConfigAppleBootView: View {
} else if newValue {
config.genericPlatform = UTMAppleConfigurationGenericPlatform()
}
if !newValue {
config.boot.hasSecureBoot = false
}
}.alert(item: $alertBootloaderSelection) { selection in
let okay = Alert.Button.default(Text("OK")) {
importBootloaderSelection = selection
@@ -93,6 +96,11 @@ struct VMConfigAppleBootView: View {
allowedContentTypes: [importBootloaderSelection == .ipsw ? .ipsw : .data],
onCompletion: selectImportedFile)
if #available(macOS 27, *), operatingSystem == .linux && config.boot.hasUefiBoot {
Toggle("Secure Boot", isOn: $config.boot.hasSecureBoot)
.help("Only boot operating systems signed by Microsoft or a trusted vendor. Most major Linux distributions are supported.")
}
if operatingSystem == .linux && !config.boot.hasUefiBoot {
Section(header: Text("Linux Settings")) {
FileBrowseField("Kernel Image", url: $config.boot.linuxKernelURL, isFileImporterPresented: $importFileShown, hasClearButton: false) {