@@ -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,32 @@ 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+ self = try Self . craft (
968+ diskPath,
969+ readOnly: readOnly,
970+ syncModeRaw: syncModeRaw,
971+ cachingModeRaw: cachingModeRaw
972+ )
973+ }
974+
975+ private init ( configuration: VZStorageDeviceConfiguration , temporaryDiskLock: FileLock ? = nil ) {
976+ self . configuration = configuration
977+ self . temporaryDiskLock = temporaryDiskLock
960978 }
961979
962- static func craft( _ diskPath: String , readOnly diskReadOnly: Bool , syncModeRaw: String , cachingModeRaw: String ) throws -> VZStorageDeviceConfiguration {
980+ private static func craft(
981+ _ diskPath: String ,
982+ readOnly diskReadOnly: Bool ,
983+ syncModeRaw: String ,
984+ cachingModeRaw: String
985+ ) throws -> AdditionalDisk {
963986 let diskURL = URL ( string: diskPath)
964987
965988 if ( [ " nbd " , " nbds " , " nbd+unix " , " nbds+unix " ] . contains ( diskURL? . scheme) ) {
@@ -974,7 +997,7 @@ struct AdditionalDisk {
974997 synchronizationMode: try VZDiskSynchronizationMode ( syncModeRaw)
975998 )
976999
977- return VZVirtioBlockDeviceConfiguration ( attachment: nbdAttachment)
1000+ return AdditionalDisk ( configuration : VZVirtioBlockDeviceConfiguration ( attachment: nbdAttachment) )
9781001 }
9791002
9801003 // Expand the tilde (~) since at this point we're dealing with a local path,
@@ -1005,13 +1028,33 @@ struct AdditionalDisk {
10051028 let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment ( fileHandle: FileHandle ( fileDescriptor: fd, closeOnDealloc: true ) ,
10061029 readOnly: diskReadOnly, synchronizationMode: try VZDiskSynchronizationMode ( syncModeRaw) )
10071030
1008- return VZVirtioBlockDeviceConfiguration ( attachment: blockAttachment)
1031+ return AdditionalDisk ( configuration : VZVirtioBlockDeviceConfiguration ( attachment: blockAttachment) )
10091032 }
10101033
10111034 // Support remote VM names in --disk command-line argument
10121035 if let remoteName = try ? RemoteName ( diskPath) {
10131036 let vmDir = try VMStorageOCI ( ) . open ( remoteName)
10141037
1038+ if vmDir. isStackedCachedImage {
1039+ // A cached stacked image has no writable top overlay. Create one in a
1040+ // disposable directory for this additional-disk attachment.
1041+ let temporaryVMDir = try VMDirectory . temporary ( )
1042+ try FileManager . default. copyItem ( at: vmDir. configURL, to: temporaryVMDir. configURL)
1043+ try FileManager . default. copyItem ( at: vmDir. nvramURL, to: temporaryVMDir. nvramURL)
1044+ try FileManager . default. copyItem ( at: vmDir. manifestURL, to: temporaryVMDir. manifestURL)
1045+ let lock = try FileLock ( lockURL: temporaryVMDir. baseURL)
1046+ try lock. lock ( )
1047+ let stack = try temporaryVMDir. diskImageStack ( )
1048+ try stack. createWritableOverlay ( )
1049+ let attachment = try stack. makeAttachment (
1050+ readOnly: diskReadOnly,
1051+ cachingMode: try VZDiskImageCachingMode ( cachingModeRaw) ?? . automatic,
1052+ synchronizationMode: try VZDiskImageSynchronizationMode ( syncModeRaw)
1053+ )
1054+
1055+ return AdditionalDisk ( configuration: VZVirtioBlockDeviceConfiguration ( attachment: attachment) , temporaryDiskLock: lock)
1056+ }
1057+
10151058 // Unfortunately, VZDiskImageStorageDeviceAttachment does not support
10161059 // FileHandle, so we can't easily clone the disk, open it and unlink(2)
10171060 // to simplify the garbage collection, so use an intermediate directory.
@@ -1024,7 +1067,7 @@ struct AdditionalDisk {
10241067
10251068 let diskImageAttachment = try VZDiskImageStorageDeviceAttachment ( url: clonedDiskURL, readOnly: diskReadOnly)
10261069
1027- return VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment)
1070+ return AdditionalDisk ( configuration : VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment) , temporaryDiskLock : lock )
10281071 }
10291072
10301073 // Error out if the disk is locked by the host (e.g. it was mounted in Finder),
@@ -1040,7 +1083,7 @@ struct AdditionalDisk {
10401083 synchronizationMode: try VZDiskImageSynchronizationMode ( syncModeRaw)
10411084 )
10421085
1043- return VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment)
1086+ return AdditionalDisk ( configuration : VZVirtioBlockDeviceConfiguration ( attachment: diskImageAttachment) )
10441087 }
10451088
10461089 static func parseOptions( _ parseFrom: String ) -> ( String , Bool , String , String ) {
0 commit comments