@@ -455,10 +455,15 @@ struct Run: AsyncParsableCommand {
455455 let provisioning = try provisioningOpts. map { try GuestProvisioningOptions ( $0) }
456456 #endif
457457
458+ // Keep these values alive while the VM runs. Some additional disks own a
459+ // lock that protects their temporary backing files from Config.gc().
460+ let additionalDisks = try additionalDisks ( )
461+ defer { withExtendedLifetime ( additionalDisks) { } }
462+
458463 vm = try VM (
459464 vmDir: vmDir,
460465 network: userSpecifiedNetwork ( vmDir: vmDir) ?? NetworkShared ( ) ,
461- additionalStorageDevices: try additionalDiskAttachments ( ) ,
466+ additionalStorageDevices: additionalDisks . map ( \ . configuration ) ,
462467 directorySharingDevices: directoryShares ( ) + rosettaDirectoryShare( ) ,
463468 serialPorts: serialPorts,
464469 suspendable: suspendable,
@@ -727,9 +732,9 @@ struct Run: AsyncParsableCommand {
727732 }
728733 }
729734
730- func additionalDiskAttachments ( ) throws -> [ VZStorageDeviceConfiguration ] {
735+ func additionalDisks ( ) throws -> [ AdditionalDisk ] {
731736 try disk. map {
732- try AdditionalDisk ( parseFrom: $0) . configuration
737+ try AdditionalDisk ( parseFrom: $0)
733738 }
734739 }
735740
@@ -952,14 +957,27 @@ struct VMView: NSViewRepresentable {
952957
953958struct AdditionalDisk {
954959 let configuration : VZStorageDeviceConfiguration
960+ // Retained for as long as the additional disk is attached, so Config.gc()
961+ // cannot remove a temporary backing file or stacked-disk directory.
962+ private let temporaryDiskLock : FileLock ?
955963
956964 init ( parseFrom: String ) throws {
957965 let ( diskPath, readOnly, syncModeRaw, cachingModeRaw) = Self . parseOptions ( parseFrom)
958966
959- self . configuration = try Self . craft ( diskPath, readOnly: readOnly, syncModeRaw: syncModeRaw, cachingModeRaw: cachingModeRaw)
967+ ( configuration, temporaryDiskLock) = try Self . craft (
968+ diskPath,
969+ readOnly: readOnly,
970+ syncModeRaw: syncModeRaw,
971+ cachingModeRaw: cachingModeRaw
972+ )
960973 }
961974
962- static func craft( _ diskPath: String , readOnly diskReadOnly: Bool , syncModeRaw: String , cachingModeRaw: String ) throws -> VZStorageDeviceConfiguration {
975+ private static func craft(
976+ _ diskPath: String ,
977+ readOnly diskReadOnly: Bool ,
978+ syncModeRaw: String ,
979+ cachingModeRaw: String
980+ ) throws -> ( VZStorageDeviceConfiguration , FileLock ? ) {
963981 let diskURL = URL ( string: diskPath)
964982
965983 if ( [ " nbd " , " nbds " , " nbd+unix " , " nbds+unix " ] . contains ( diskURL? . scheme) ) {
@@ -974,7 +992,7 @@ struct AdditionalDisk {
974992 synchronizationMode: try VZDiskSynchronizationMode ( syncModeRaw)
975993 )
976994
977- return VZVirtioBlockDeviceConfiguration ( attachment: nbdAttachment)
995+ return ( VZVirtioBlockDeviceConfiguration ( attachment: nbdAttachment) , nil )
978996 }
979997
980998 // Expand the tilde (~) since at this point we're dealing with a local path,
@@ -1005,13 +1023,33 @@ struct AdditionalDisk {
10051023 let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment ( fileHandle: FileHandle ( fileDescriptor: fd, closeOnDealloc: true ) ,
10061024 readOnly: diskReadOnly, synchronizationMode: try VZDiskSynchronizationMode ( syncModeRaw) )
10071025
1008- return VZVirtioBlockDeviceConfiguration ( attachment: blockAttachment)
1026+ return ( VZVirtioBlockDeviceConfiguration ( attachment: blockAttachment) , nil )
10091027 }
10101028
10111029 // Support remote VM names in --disk command-line argument
10121030 if let remoteName = try ? RemoteName ( diskPath) {
10131031 let vmDir = try VMStorageOCI ( ) . open ( remoteName)
10141032
1033+ if vmDir. isStackedCachedImage {
1034+ // A cached stacked image has no writable top overlay. Create one in a
1035+ // disposable directory for this additional-disk attachment.
1036+ let temporaryVMDir = try VMDirectory . temporary ( )
1037+ try FileManager . default. copyItem ( at: vmDir. configURL, to: temporaryVMDir. configURL)
1038+ try FileManager . default. copyItem ( at: vmDir. nvramURL, to: temporaryVMDir. nvramURL)
1039+ try FileManager . default. copyItem ( at: vmDir. manifestURL, to: temporaryVMDir. manifestURL)
1040+ let lock = try FileLock ( lockURL: temporaryVMDir. baseURL)
1041+ try lock. lock ( )
1042+ let stack = try temporaryVMDir. diskImageStack ( )
1043+ try stack. createWritableOverlay ( )
1044+ let attachment = try stack. makeAttachment (
1045+ readOnly: diskReadOnly,
1046+ cachingMode: try VZDiskImageCachingMode ( cachingModeRaw) ?? . automatic,
1047+ synchronizationMode: try VZDiskImageSynchronizationMode ( syncModeRaw)
1048+ )
1049+
1050+ return ( VZVirtioBlockDeviceConfiguration ( attachment: attachment) , lock)
1051+ }
1052+
10151053 // Unfortunately, VZDiskImageStorageDeviceAttachment does not support
10161054 // FileHandle, so we can't easily clone the disk, open it and unlink(2)
10171055 // to simplify the garbage collection, so use an intermediate directory.
@@ -1024,7 +1062,7 @@ struct AdditionalDisk {
10241062
10251063 let diskImageAttachment = try VZDiskImageStorageDeviceAttachment ( url: clonedDiskURL, readOnly: diskReadOnly)
10261064
1027- return VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment)
1065+ return ( VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment) , lock )
10281066 }
10291067
10301068 // Error out if the disk is locked by the host (e.g. it was mounted in Finder),
@@ -1040,7 +1078,7 @@ struct AdditionalDisk {
10401078 synchronizationMode: try VZDiskImageSynchronizationMode ( syncModeRaw)
10411079 )
10421080
1043- return VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment)
1081+ return ( VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment) , nil )
10441082 }
10451083
10461084 static func parseOptions( _ parseFrom: String ) -> ( String , Bool , String , String ) {
0 commit comments