diff --git a/Sources/Containerization/AttachedFilesystem.swift b/Sources/Containerization/AttachedFilesystem.swift index ebe42ab5..8c3d1bae 100644 --- a/Sources/Containerization/AttachedFilesystem.swift +++ b/Sources/Containerization/AttachedFilesystem.swift @@ -16,6 +16,7 @@ import ContainerizationExtras import ContainerizationOCI +import Foundation /// A filesystem that was attached and able to be mounted inside the runtime environment. public struct AttachedFilesystem: Sendable { @@ -32,7 +33,16 @@ public struct AttachedFilesystem: Sendable { public init(mount: Mount, allocator: any AddressAllocator) throws { switch mount.type { case "virtiofs": - let name = try hashMountSource(source: mount.source) + let name: String = try { + guard mount.isFile else { + return try hashMountSource(source: mount.source) + } + + let fileTag = try hashMountSource(source: mount.source) + let directory = try hashMountSource(source: mount.hardlinkDirectory!) + + return URL(string: directory)!.appendingPathComponent(fileTag).path + }() self.source = name case "ext4": let char = try allocator.allocate() diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 72de285d..9e14b3d2 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -419,6 +419,19 @@ public struct ContainerManager: Sendable { } config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log")) try configuration(&config) + + let sharedDir = sharedFileDirectory(id) + for i in config.mounts.indices { + if config.mounts[i].isFile { + let file = URL(fileURLWithPath: config.mounts[i].source) + let fileTag = try hashMountSource(source: config.mounts[i].source) + let hardlink = sharedDir.appendingPathComponent(fileTag) + + try FileManager.default.linkItem(at: file, to: hardlink) + config.mounts[i].hardlinkDirectory = sharedDir.path + config.mounts[i].options.append("bind") + } + } } } @@ -433,9 +446,15 @@ public struct ContainerManager: Sendable { private func createContainerRoot(_ id: String) throws -> URL { let path = containerRoot.appendingPathComponent(id) try FileManager.default.createDirectory(at: path, withIntermediateDirectories: false) + try FileManager.default.createDirectory(at: sharedFileDirectory(id), withIntermediateDirectories: false) + return path } + private func sharedFileDirectory(_ id: String) -> URL { + containerRoot.appendingPathComponent(id).appendingPathComponent("virtiofs") + } + private func unpack(image: Image, destination: URL, size: UInt64) async throws -> Mount { do { let unpacker = EXT4Unpacker(blockSizeInBytes: size) diff --git a/Sources/Containerization/Mount.swift b/Sources/Containerization/Mount.swift index 6aae7f0a..3829cfef 100644 --- a/Sources/Containerization/Mount.swift +++ b/Sources/Containerization/Mount.swift @@ -36,6 +36,8 @@ public struct Mount: Sendable { /// should create for this specific mount (virtioblock /// virtiofs etc.). public let runtimeOptions: RuntimeOptions + /// hardlink directory path if the source is file. + public var hardlinkDirectory: String? /// A type representing a "hint" of what type /// of mount this really is (block, directory, purely @@ -144,7 +146,15 @@ extension Mount { throw ContainerizationError(.notFound, message: "directory \(source) does not exist") } - let name = try hashMountSource(source: self.source) + let source = isFile ? self.hardlinkDirectory! : self.source + let name = try hashMountSource(source: source) + guard + !config.directorySharingDevices.contains( + where: { ($0 as? VZVirtioFileSystemDeviceConfiguration)?.tag == name }) + else { + break + } + let urlSource = URL(fileURLWithPath: source) let device = VZVirtioFileSystemDeviceConfiguration(tag: name) diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index 3e1baa30..7735b121 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -423,6 +423,16 @@ extension Mount { var isBlock: Bool { type == "ext4" } + + var isFile: Bool { + guard self.type == "virtiofs" else { + return false + } + + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: self.source, isDirectory: &isDirectory) + return exists && !isDirectory.boolValue + } } extension Kernel { diff --git a/Sources/ContainerizationOS/Mount/Mount.swift b/Sources/ContainerizationOS/Mount/Mount.swift index e62bb9f0..2b7a30c4 100644 --- a/Sources/ContainerizationOS/Mount/Mount.swift +++ b/Sources/ContainerizationOS/Mount/Mount.swift @@ -115,6 +115,10 @@ extension Mount { return false } + public var isSharedFile: Bool { + type == "" && self.options.contains("bind") + } + /// Mount the mount relative to `root` with the current set of data in the object. /// Optionally provide `createWithPerms` to set the permissions for the directory that /// it will be mounted at. @@ -146,12 +150,25 @@ extension Mount { // Ensure propagation type change flags aren't included in other calls. let originalFlags = opts.flags & ~(propagationTypes) - let targetURL = URL(fileURLWithPath: self.target) + // TODO: What are lines 153, 154 for? + // let targetURL = URL(fileURLWithPath: self.target) + // let targetParent = targetURL.deletingLastPathComponent().path + // if let perms = createWithPerms { + // try mkdirAll(targetParent, perms) + // } + + let targetURL = URL(fileURLWithPath: target) let targetParent = targetURL.deletingLastPathComponent().path if let perms = createWithPerms { try mkdirAll(targetParent, perms) } - try mkdirAll(target, 0o755) + + if self.isSharedFile { + try mkdirAll(targetParent, 0o755) + createFile(target) + } else { + try mkdirAll(target, 0o755) + } if opts.flags & Int32(MS_REMOUNT) == 0 || !dataString.isEmpty { guard _mount(self.source, target, self.type, UInt(originalFlags), dataString) == 0 else { @@ -186,6 +203,14 @@ extension Mount { ) } + private func createFile(_ name: String) { + _ = FileManager.default.createFile( + atPath: name, + contents: nil, + attributes: nil + ) + } + private func parseMountOptions() -> MountOptions { var mountOpts = MountOptions() for option in self.options { diff --git a/vminitd/Sources/vmexec/Mount.swift b/vminitd/Sources/vmexec/Mount.swift index 0a269065..806e0c76 100644 --- a/vminitd/Sources/vmexec/Mount.swift +++ b/vminitd/Sources/vmexec/Mount.swift @@ -22,15 +22,17 @@ import Musl struct ContainerMount { private let mounts: [ContainerizationOCI.Mount] private let rootfs: String + private let sharedFileDirectory: URL - init(rootfs: String, mounts: [ContainerizationOCI.Mount]) { + init(rootfs: String, sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) { self.rootfs = rootfs + self.sharedFileDirectory = sharedFileDirectory self.mounts = mounts } func mountToRootfs() throws { for m in self.mounts { - let osMount = m.toOSMount() + let osMount = m.toOSMount(sharedFileDirectory) try osMount.mount(root: self.rootfs) } } @@ -55,10 +57,23 @@ struct ContainerMount { } extension ContainerizationOCI.Mount { - func toOSMount() -> ContainerizationOS.Mount { - ContainerizationOS.Mount( - type: self.type, - source: self.source, + var isSharedFile: Bool { + type == "virtiofs" && options.contains("bind") + } + + func toOSMount(_ sharedFileDirectory: URL) -> ContainerizationOS.Mount { + let type = isSharedFile ? "" : self.type + let source = { + guard isSharedFile else { + return self.source + } + let name = URL(string: self.source)!.lastPathComponent + return sharedFileDirectory.appendingPathComponent(name).path + }() + + return ContainerizationOS.Mount( + type: type, + source: source, target: self.destination, options: self.options ) diff --git a/vminitd/Sources/vmexec/RunCommand.swift b/vminitd/Sources/vmexec/RunCommand.swift index 615e8d58..619aa05f 100644 --- a/vminitd/Sources/vmexec/RunCommand.swift +++ b/vminitd/Sources/vmexec/RunCommand.swift @@ -46,9 +46,12 @@ struct RunCommand: ParsableCommand { } private func childRootSetup(rootfs: ContainerizationOCI.Root, mounts: [ContainerizationOCI.Mount], log: Logger) throws { + let sharedFileDirectory = URL(fileURLWithPath: bundlePath).appendingPathComponent("virtiofs") + // setup rootfs try prepareRoot(rootfs: rootfs.path) - try mountRootfs(rootfs: rootfs.path, mounts: mounts) + try prepareSharedFiles(sharedFileDirectory: sharedFileDirectory, mounts: mounts) + try mountRootfs(rootfs: rootfs.path, sharedFileDirectory: sharedFileDirectory, mounts: mounts) try setDevSymlinks(rootfs: rootfs.path) try pivotRoot(rootfs: rootfs.path) @@ -185,8 +188,8 @@ struct RunCommand: ParsableCommand { } } - private func mountRootfs(rootfs: String, mounts: [ContainerizationOCI.Mount]) throws { - let containerMount = ContainerMount(rootfs: rootfs, mounts: mounts) + private func mountRootfs(rootfs: String, sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) throws { + let containerMount = ContainerMount(rootfs: rootfs, sharedFileDirectory: sharedFileDirectory, mounts: mounts) try containerMount.mountToRootfs() try containerMount.configureConsole() } @@ -201,6 +204,17 @@ struct RunCommand: ParsableCommand { } } + private func prepareSharedFiles(sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) throws { + if let source = mounts.first(where: { $0.isSharedFile })?.source { + let tag = URL(string: source)!.deletingLastPathComponent().path + + try FileManager.default.createDirectory(at: sharedFileDirectory, withIntermediateDirectories: false) + guard mount(tag, sharedFileDirectory.path, "virtiofs", UInt(0), nil) == 0 else { + throw App.Errno(stage: "mount(shared)") + } + } + } + private func setDevSymlinks(rootfs: String) throws { let links: [(src: String, dst: String)] = [ ("/proc/self/fd", "/dev/fd"),